diff --git a/.eslintrc.json b/.eslintrc.json index 8761c1c1813..df3c5ad560c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -653,6 +653,20 @@ "**/vs/workbench/services/**/{common,browser}/**" ] }, + { + "target": "**/vs/workbench/contrib/notebook/common/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,worker}/**", + "**/vs/platform/**/common/**", + "**/vs/editor/**", + "**/vs/workbench/common/**", + "**/vs/workbench/api/common/**", + "**/vs/workbench/services/**/common/**", + "**/vs/workbench/contrib/**/common/**" + ] + }, { "target": "**/vs/workbench/contrib/**/common/**", "restrictions": [ diff --git a/.github/classifier.json b/.github/classifier.json index bb313eebafb..33c179d3237 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -1,5 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/vscode-github-triage-actions/master/classifier-deep/apply/apply-labels/deep-classifier-config.schema.json", + "vacation": ["joaomoreno"], "assignees": { "JacksonKearl": {"accuracy": 0.5} }, @@ -133,7 +134,7 @@ "snippets": {"assign": ["jrieken"]}, "splitview": {"assign": ["joaomoreno"]}, "suggest": {"assign": ["jrieken"]}, - "tasks": {"assign": ["alexr00"]}, + "tasks": {"assign": ["alexr00"], "accuracy": 0.85}, "telemetry": {"assign": []}, "themes": {"assign": ["aeschli"]}, "timeline": {"assign": ["eamodio"]}, diff --git a/.github/workflows/author-verified.yml b/.github/workflows/author-verified.yml index 7114f351353..167f27a5490 100644 --- a/.github/workflows/author-verified.yml +++ b/.github/workflows/author-verified.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v31 + ref: v34 path: ./actions - name: Install Actions if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'author-verification-requested') diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5e990067f1d..017708844a4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,6 +1,8 @@ name: "Code Scanning" -on: [push, pull_request] +on: + schedule: + - cron: '0 0 * * 2' jobs: CodeQL-Build: diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 7036a4937d4..61c82aa73ca 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -13,7 +13,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v31 + ref: v34 - name: Install Actions run: npm install --production --prefix ./actions - name: Run Commands diff --git a/.github/workflows/deep-classifier-monitor.yml b/.github/workflows/deep-classifier-monitor.yml index 7aa6d9de22f..9425ae0f1f2 100644 --- a/.github/workflows/deep-classifier-monitor.yml +++ b/.github/workflows/deep-classifier-monitor.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v31 + ref: v34 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/deep-classifier-runner.yml b/.github/workflows/deep-classifier-runner.yml index 4ac237e5f83..2f598745e49 100644 --- a/.github/workflows/deep-classifier-runner.yml +++ b/.github/workflows/deep-classifier-runner.yml @@ -1,7 +1,7 @@ name: "Deep Classifier: Runner" on: schedule: - - cron: 0/30 * * * * + - cron: 0 * * * * repository_dispatch: types: [trigger-deep-classifier-runner] @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v31 + ref: v34 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions @@ -24,7 +24,7 @@ jobs: uses: ./actions/classifier-deep/apply/fetch-sources with: # slightly overlapping to protect against issues slipping through the cracks if a run is delayed - from: 40 + from: 80 until: 5 configPath: classifier blobContainerName: vscode-issue-classifier diff --git a/.github/workflows/deep-classifier-scraper.yml b/.github/workflows/deep-classifier-scraper.yml index c93f2b5352f..b9cb6ec3cd3 100644 --- a/.github/workflows/deep-classifier-scraper.yml +++ b/.github/workflows/deep-classifier-scraper.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v31 + ref: v34 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/english-please.yml b/.github/workflows/english-please.yml index 1ecc532ce88..fdfd548291d 100644 --- a/.github/workflows/english-please.yml +++ b/.github/workflows/english-please.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v31 + ref: v34 path: ./actions - name: Install Actions if: contains(github.event.issue.labels.*.name, '*english-please') diff --git a/.github/workflows/feature-request.yml b/.github/workflows/feature-request.yml index cdd65c77202..12af1423506 100644 --- a/.github/workflows/feature-request.yml +++ b/.github/workflows/feature-request.yml @@ -18,7 +18,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v31 + ref: v34 - name: Install Actions if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') run: npm install --production --prefix ./actions diff --git a/.github/workflows/latest-release-monitor.yml b/.github/workflows/latest-release-monitor.yml index a00d3554147..a5777e3a7f1 100644 --- a/.github/workflows/latest-release-monitor.yml +++ b/.github/workflows/latest-release-monitor.yml @@ -14,7 +14,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v31 + ref: v34 - name: Install Actions run: npm install --production --prefix ./actions - name: Install Storage Module diff --git a/.github/workflows/locker.yml b/.github/workflows/locker.yml index d805f6a6428..64ac30d3717 100644 --- a/.github/workflows/locker.yml +++ b/.github/workflows/locker.yml @@ -14,7 +14,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v31 + ref: v34 - name: Install Actions run: npm install --production --prefix ./actions - name: Run Locker diff --git a/.github/workflows/needs-more-info-closer.yml b/.github/workflows/needs-more-info-closer.yml index d143dec9536..018caf24fe0 100644 --- a/.github/workflows/needs-more-info-closer.yml +++ b/.github/workflows/needs-more-info-closer.yml @@ -14,16 +14,17 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v31 + ref: v34 - name: Install Actions run: npm install --production --prefix ./actions - name: Run Needs More Info Closer uses: ./actions/needs-more-info-closer with: appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} label: needs more info 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: 100 + 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-label.yml b/.github/workflows/on-label.yml index 97c8ee18e8e..446c041ebd3 100644 --- a/.github/workflows/on-label.yml +++ b/.github/workflows/on-label.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v31 + ref: v34 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/on-open.yml b/.github/workflows/on-open.yml index e8c41404d99..e4f0434dbdc 100644 --- a/.github/workflows/on-open.yml +++ b/.github/workflows/on-open.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v31 + ref: v34 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/release-pipeline-labeler.yml b/.github/workflows/release-pipeline-labeler.yml index bc45221133f..14f74f581dd 100644 --- a/.github/workflows/release-pipeline-labeler.yml +++ b/.github/workflows/release-pipeline-labeler.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v31 + ref: v34 path: ./actions - name: Checkout Repo if: github.event_name != 'issues' diff --git a/.github/workflows/rich-navigation.yml b/.github/workflows/rich-navigation.yml index 185c770dd0f..38afb0d7f49 100644 --- a/.github/workflows/rich-navigation.yml +++ b/.github/workflows/rich-navigation.yml @@ -10,16 +10,10 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v2 - - name: Use Node.js - uses: actions/setup-node@v1 - name: Install dependencies run: yarn --frozen-lockfile env: CHILD_CONCURRENCY: 1 - - name: Install .NET Core 2.2 - uses: actions/setup-dotnet@v1.5.0 - with: - dotnet-version: 2.2 - uses: microsoft/RichCodeNavIndexer@v0.1 with: languages: typescript diff --git a/.github/workflows/test-plan-item-validator.yml b/.github/workflows/test-plan-item-validator.yml index 2dbf0e45871..4365afa03e5 100644 --- a/.github/workflows/test-plan-item-validator.yml +++ b/.github/workflows/test-plan-item-validator.yml @@ -14,7 +14,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v31 + ref: v34 - 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 diff --git a/.vscode/launch.json b/.vscode/launch.json index 577b733df80..33801a60ee3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -226,7 +226,9 @@ "--no-cached-data", ], "webRoot": "${workspaceFolder}", - // Settings for js-debug: + "cascadeTerminateToConfigurations": [ + "Attach to Extension Host" + ], "userDataDir": false, "pauseForSourceMap": false, "outFiles": [ @@ -239,7 +241,7 @@ "type": "node", "request": "launch", "name": "VS Code (Web)", - "program": "${workspaceFolder}/resources/serverless/code-web.js", + "program": "${workspaceFolder}/resources/web/code-web.js", "presentation": { "group": "0_vscode", "order": 2 @@ -436,6 +438,7 @@ "Attach to Extension Host", "Attach to Shared Process", ], + "preLaunchTask": "Ensure Prelaunch Dependencies", "presentation": { "group": "0_vscode", "order": 1 diff --git a/.vscode/notebooks/api.github-issues b/.vscode/notebooks/api.github-issues index f36bb397581..ad2d028465e 100644 --- a/.vscode/notebooks/api.github-issues +++ b/.vscode/notebooks/api.github-issues @@ -8,7 +8,7 @@ { "kind": 2, "language": "github-issues", - "value": "$repo=repo:microsoft/vscode\n$milestone=milestone:\"July 2020\"", + "value": "$repo=repo:microsoft/vscode\n$milestone=milestone:\"August 2020\"", "editable": true }, { diff --git a/.vscode/notebooks/inbox.github-issues b/.vscode/notebooks/inbox.github-issues index 979da52c04d..e2e9e8a1cbb 100644 --- a/.vscode/notebooks/inbox.github-issues +++ b/.vscode/notebooks/inbox.github-issues @@ -8,17 +8,20 @@ { "kind": 2, "language": "github-issues", - "value": "$inbox=repo:microsoft/vscode is:open no:assignee -label:feature-request -label:testplan-item -label:plan-item " + "value": "$inbox=repo:microsoft/vscode is:open no:assignee -label:feature-request -label:testplan-item -label:plan-item ", + "editable": true }, { "kind": 1, "language": "markdown", - "value": "## Inbox tracking and Issue triage" + "value": "## Inbox tracking and Issue triage", + "editable": true }, { "kind": 1, "language": "markdown", - "value": "New issues or pull requests submitted by the community are initially triaged by an [automatic classification bot](https://github.com/microsoft/vscode-github-triage-actions/tree/master/classifier-deep). Issues that the bot does not correctly triage are then triaged by a team member. The team rotates the inbox tracker on a weekly basis.\n\nA [mirror](https://github.com/JacksonKearl/testissues/issues) of the VS Code issue stream is available with details about how the bot classifies issues, including feature-area classifications and confidence ratings. Per-category confidence thresholds and feature-area ownership data is maintained in [.github/classifier.json](https://github.com/microsoft/vscode/blob/master/.github/classifier.json). \n\nšŸ’” The bot is being run through a GitHub action that runs every 30 minutes. Give the bot the opportunity to classify an issue before doing it manually.\n\n### Inbox Tracking\n\nThe inbox tracker is responsible for the [global inbox](https://github.com/Microsoft/vscode/issues?utf8=%E2%9C%93&q=is%3Aopen+no%3Aassignee+-label%3Afeature-request+-label%3Atestplan-item+-label%3Aplan-item) containing all **open issues and pull requests** that\n- are neither **feature requests** nor **test plan items** nor **plan items** and\n- have **no owner assignment**.\n\nThe **inbox tracker** may perform any step described in our [issue triaging documentation](https://github.com/microsoft/vscode/wiki/Issues-Triaging) but its main responsibility is to route issues to the actual feature area owner.\n\nFeature area owners track the **feature area inbox** containing all **open issues and pull requests** that\n- are personally assigned to them and are not assigned to any milestone\n- are labeled with their feature area label and are not assigned to any milestone.\nThis secondary triage may involve any of the steps described in our [issue triaging documentation](https://github.com/microsoft/vscode/wiki/Issues-Triaging) and results in a fully triaged or closed issue.\n\nThe [github triage extension](https://github.com/microsoft/vscode-github-triage-extension) can be used to assist with triaging ā€” it provides a \"Command Palette\"-style list of triaging actions like assignment, labeling, and triggers for various bot actions." + "value": "New issues or pull requests submitted by the community are initially triaged by an [automatic classification bot](https://github.com/microsoft/vscode-github-triage-actions/tree/master/classifier-deep). Issues that the bot does not correctly triage are then triaged by a team member. The team rotates the inbox tracker on a weekly basis.\n\nA [mirror](https://github.com/JacksonKearl/testissues/issues) of the VS Code issue stream is available with details about how the bot classifies issues, including feature-area classifications and confidence ratings. Per-category confidence thresholds and feature-area ownership data is maintained in [.github/classifier.json](https://github.com/microsoft/vscode/blob/master/.github/classifier.json). \n\nšŸ’” The bot is being run through a GitHub action that runs every 30 minutes. Give the bot the opportunity to classify an issue before doing it manually.\n\n### Inbox Tracking\n\nThe inbox tracker is responsible for the [global inbox](https://github.com/Microsoft/vscode/issues?utf8=%E2%9C%93&q=is%3Aopen+no%3Aassignee+-label%3Afeature-request+-label%3Atestplan-item+-label%3Aplan-item) containing all **open issues and pull requests** that\n- are neither **feature requests** nor **test plan items** nor **plan items** and\n- have **no owner assignment**.\n\nThe **inbox tracker** may perform any step described in our [issue triaging documentation](https://github.com/microsoft/vscode/wiki/Issues-Triaging) but its main responsibility is to route issues to the actual feature area owner.\n\nFeature area owners track the **feature area inbox** containing all **open issues and pull requests** that\n- are personally assigned to them and are not assigned to any milestone\n- are labeled with their feature area label and are not assigned to any milestone.\nThis secondary triage may involve any of the steps described in our [issue triaging documentation](https://github.com/microsoft/vscode/wiki/Issues-Triaging) and results in a fully triaged or closed issue.\n\nThe [github triage extension](https://github.com/microsoft/vscode-github-triage-extension) can be used to assist with triaging ā€” it provides a \"Command Palette\"-style list of triaging actions like assignment, labeling, and triggers for various bot actions.", + "editable": true }, { "kind": 1, diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index dc6d33365d7..d1452276f6e 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -8,7 +8,7 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks\n\n// current milestone name\n$milestone=milestone:\"June 2020\"", + "value": "// list of repos we work in\n$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks\n\n// current milestone name\n$milestone=milestone:\"August 2020\"", "editable": true }, { diff --git a/.vscode/searches/es6.code-search b/.vscode/searches/es6.code-search index 9cf8cf0b264..6ab0d14c5ac 100644 --- a/.vscode/searches/es6.code-search +++ b/.vscode/searches/es6.code-search @@ -34,11 +34,11 @@ src/vs/base/common/arrays.ts: 420 */ 421 export function first(array: ReadonlyArray, fn: (item: T) => boolean, notFoundValue: T): T; - 569 - 570 /** - 571: * @deprecated ES6: use `Array.find` - 572 */ - 573 export function find(arr: ArrayLike, predicate: (value: T, index: number, arr: ArrayLike) => any): T | undefined { + 568 + 569 /** + 570: * @deprecated ES6: use `Array.find` + 571 */ + 572 export function find(arr: ArrayLike, predicate: (value: T, index: number, arr: ArrayLike) => any): T | undefined { src/vs/base/common/objects.ts: 115 diff --git a/.yarnrc b/.yarnrc index 135e10442a7..3c6eccfb102 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,3 @@ disturl "https://atom.io/download/electron" -target "7.3.2" +target "9.2.1" runtime "electron" diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index ac17c9eaa64..4cf2d042f80 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -33,7 +33,7 @@ This project incorporates components from the projects listed below. The origina 26. Jxck/assert version 1.0.0 (https://github.com/Jxck/assert) 27. language-docker (https://github.com/moby/moby) 28. language-less version 0.34.2 (https://github.com/atom/language-less) -29. language-php version 0.44.4 (https://github.com/atom/language-php) +29. language-php version 0.44.5 (https://github.com/atom/language-php) 30. language-rust version 0.4.12 (https://github.com/zargony/atom-language-rust) 31. MagicStack/MagicPython version 1.1.1 (https://github.com/MagicStack/MagicPython) 32. marked version 0.6.2 (https://github.com/markedjs/marked) diff --git a/build/azure-pipelines/darwin/continuous-build-darwin.yml b/build/azure-pipelines/darwin/continuous-build-darwin.yml index 5785de63367..631b9af7f10 100644 --- a/build/azure-pipelines/darwin/continuous-build-darwin.yml +++ b/build/azure-pipelines/darwin/continuous-build-darwin.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.13.0" + versionSpec: "12.14.1" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: @@ -50,7 +50,7 @@ steps: displayName: Run Unit Tests (Electron) - script: | - yarn test-browser --browser chromium --browser webkit --browser firefox + yarn test-browser --browser chromium --browser webkit --browser firefox --tfs "Browser Unit Tests" displayName: Run Unit Tests (Browser) - script: | diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index ea286ef1418..3b186bb1136 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -21,7 +21,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.13.0" + versionSpec: "12.14.1" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: @@ -101,7 +101,7 @@ steps: - script: | set -e - yarn test-browser --build --browser chromium --browser webkit --browser firefox + yarn test-browser --build --browser chromium --browser webkit --browser firefox --tfs "Browser Unit Tests" displayName: Run unit tests (Browser) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) @@ -118,6 +118,13 @@ steps: displayName: Run integration tests (Electron) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) +- script: | + set -e + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin" \ + ./resources/server/test/test-web-integration.sh --browser webkit + displayName: Run integration tests (Browser) + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - script: | set -e APP_ROOT=$(agent.builddirectory)/VSCode-darwin @@ -128,13 +135,6 @@ steps: displayName: Run remote integration tests (Electron) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) -- script: | - set -e - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin" \ - ./resources/server/test/test-web-integration.sh --browser webkit - displayName: Run integration tests (Browser) - condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - script: | set -e APP_ROOT=$(agent.builddirectory)/VSCode-darwin @@ -160,6 +160,13 @@ steps: continueOnError: true condition: failed() +- task: PublishTestResults@2 + displayName: Publish Tests Results + inputs: + testResultsFiles: '*-results.xml' + searchFolder: '$(Build.ArtifactStagingDirectory)/test-results' + condition: succeededOrFailed() + - script: | set -e security create-keychain -p pwd $(agent.tempdirectory)/buildagent.keychain diff --git a/build/azure-pipelines/distro-build.yml b/build/azure-pipelines/distro-build.yml index 4689451b54e..f9bdf7fef8e 100644 --- a/build/azure-pipelines/distro-build.yml +++ b/build/azure-pipelines/distro-build.yml @@ -8,7 +8,7 @@ pr: steps: - task: NodeTool@0 inputs: - versionSpec: "12.13.0" + versionSpec: "12.14.1" - task: AzureKeyVault@1 displayName: 'Azure Key Vault: Get Secrets' diff --git a/build/azure-pipelines/exploration-build.yml b/build/azure-pipelines/exploration-build.yml index 370c56fa6a1..a8747353c37 100644 --- a/build/azure-pipelines/exploration-build.yml +++ b/build/azure-pipelines/exploration-build.yml @@ -11,7 +11,7 @@ pr: steps: - task: NodeTool@0 inputs: - versionSpec: "12.13.0" + versionSpec: "12.14.1" - task: AzureKeyVault@1 displayName: 'Azure Key Vault: Get Secrets' diff --git a/build/azure-pipelines/linux/continuous-build-linux.yml b/build/azure-pipelines/linux/continuous-build-linux.yml index fdd4c305cda..41225110f37 100644 --- a/build/azure-pipelines/linux/continuous-build-linux.yml +++ b/build/azure-pipelines/linux/continuous-build-linux.yml @@ -10,7 +10,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.13.0" + versionSpec: "12.14.1" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: @@ -63,7 +63,7 @@ steps: displayName: Run Unit Tests (Electron) - script: | - DISPLAY=:10 yarn test-browser --browser chromium + DISPLAY=:10 yarn test-browser --browser chromium --tfs "Browser Unit Tests" displayName: Run Unit Tests (Browser) - script: | diff --git a/build/azure-pipelines/linux/product-build-linux-multiarch.yml b/build/azure-pipelines/linux/product-build-linux-multiarch.yml index 485f8dcfba7..258f87ea3d2 100644 --- a/build/azure-pipelines/linux/product-build-linux-multiarch.yml +++ b/build/azure-pipelines/linux/product-build-linux-multiarch.yml @@ -21,7 +21,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.13.0" + versionSpec: "12.14.1" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index 5d7bccf467f..21d963042c8 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -21,7 +21,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.13.0" + versionSpec: "12.14.1" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: @@ -106,7 +106,7 @@ steps: - script: | set -e - DISPLAY=:10 yarn test-browser --build --browser chromium + DISPLAY=:10 yarn test-browser --build --browser chromium --tfs "Browser Unit Tests" displayName: Run unit tests (Browser) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) @@ -123,6 +123,13 @@ steps: displayName: Run integration tests (Electron) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) +- script: | + set -e + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-linux-x64" \ + DISPLAY=:10 ./resources/server/test/test-web-integration.sh --browser chromium + displayName: Run integration tests (Browser) + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - script: | set -e APP_ROOT=$(agent.builddirectory)/VSCode-linux-x64 @@ -133,13 +140,6 @@ steps: displayName: Run remote integration tests (Electron) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) -- script: | - set -e - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-linux-x64" \ - DISPLAY=:10 ./resources/server/test/test-web-integration.sh --browser chromium - displayName: Run integration tests (Browser) - condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - task: PublishPipelineArtifact@0 inputs: artifactName: crash-dump-linux @@ -148,6 +148,13 @@ steps: continueOnError: true condition: failed() +- task: PublishTestResults@2 + displayName: Publish Tests Results + inputs: + testResultsFiles: '*-results.xml' + searchFolder: '$(Build.ArtifactStagingDirectory)/test-results' + condition: succeededOrFailed() + - script: | set -e yarn gulp "vscode-linux-x64-build-deb" diff --git a/build/azure-pipelines/linux/snap-build-linux.yml b/build/azure-pipelines/linux/snap-build-linux.yml index a530499b313..39c39e86c9e 100644 --- a/build/azure-pipelines/linux/snap-build-linux.yml +++ b/build/azure-pipelines/linux/snap-build-linux.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.13.0" + versionSpec: "12.14.1" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index 7b6d2bcbbde..1f3a0805086 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -1,157 +1,3 @@ -resources: - containers: - - container: vscode-x64 - image: vscodehub.azurecr.io/vscode-linux-build-agent:x64 - endpoint: VSCodeHub - - container: snapcraft - image: snapcore/snapcraft:stable - -jobs: -- job: Compile - pool: - vmImage: 'Ubuntu-16.04' - container: vscode-x64 - steps: - - template: product-compile.yml - -- job: Windows - condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false'), eq(variables['VSCODE_BUILD_WIN32'], 'true')) - pool: - vmImage: VS2017-Win2016 - variables: - VSCODE_ARCH: x64 - dependsOn: - - Compile - steps: - - template: win32/product-build-win32.yml - -- job: Windows32 - condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false'), eq(variables['VSCODE_BUILD_WIN32_32BIT'], 'true')) - pool: - vmImage: VS2017-Win2016 - variables: - VSCODE_ARCH: ia32 - dependsOn: - - Compile - steps: - - template: win32/product-build-win32.yml - -- job: WindowsARM64 - condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false'), eq(variables['VSCODE_BUILD_WIN32_ARM64'], 'true')) - pool: - vmImage: VS2017-Win2016 - variables: - VSCODE_ARCH: arm64 - dependsOn: - - Compile - steps: - - template: win32/product-build-win32-arm64.yml - -- job: Linux - condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false'), eq(variables['VSCODE_BUILD_LINUX'], 'true')) - pool: - vmImage: 'Ubuntu-16.04' - container: vscode-x64 - dependsOn: - - Compile - steps: - - template: linux/product-build-linux.yml - -- job: LinuxSnap - condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false'), eq(variables['VSCODE_BUILD_LINUX'], 'true')) - pool: - vmImage: 'Ubuntu-16.04' - container: snapcraft - dependsOn: Linux - steps: - - template: linux/snap-build-linux.yml - -- job: LinuxArmhf - condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false'), eq(variables['VSCODE_BUILD_LINUX_ARMHF'], 'true')) - pool: - vmImage: 'Ubuntu-16.04' - variables: - VSCODE_ARCH: armhf - dependsOn: - - Compile - steps: - - template: linux/product-build-linux-multiarch.yml - -- job: LinuxArm64 - condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false'), eq(variables['VSCODE_BUILD_LINUX_ARM64'], 'true')) - pool: - vmImage: 'Ubuntu-16.04' - variables: - VSCODE_ARCH: arm64 - dependsOn: - - Compile - steps: - - template: linux/product-build-linux-multiarch.yml - -- job: LinuxAlpine - condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false'), eq(variables['VSCODE_BUILD_LINUX_ALPINE'], 'true')) - pool: - vmImage: 'Ubuntu-16.04' - variables: - VSCODE_ARCH: alpine - dependsOn: - - Compile - steps: - - template: linux/product-build-linux-multiarch.yml - -- job: LinuxWeb - condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false'), eq(variables['VSCODE_BUILD_WEB'], 'true')) - pool: - vmImage: 'Ubuntu-16.04' - variables: - VSCODE_ARCH: x64 - dependsOn: - - Compile - steps: - - template: web/product-build-web.yml - -- job: macOS - condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false'), eq(variables['VSCODE_BUILD_MACOS'], 'true')) - pool: - vmImage: macOS-latest - dependsOn: - - Compile - steps: - - template: darwin/product-build-darwin.yml - -- job: Release - condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false'), or(eq(variables['VSCODE_RELEASE'], 'true'), and(or(eq(variables['VSCODE_QUALITY'], 'insider'), eq(variables['VSCODE_QUALITY'], 'exploration')), eq(variables['Build.Reason'], 'Schedule')))) - pool: - vmImage: 'Ubuntu-16.04' - dependsOn: - - Windows - - Windows32 - - Linux - - LinuxSnap - - LinuxArmhf - - LinuxArm64 - - LinuxAlpine - - macOS - steps: - - template: release.yml - -- job: Mooncake - pool: - vmImage: 'Ubuntu-16.04' - condition: and(succeededOrFailed(), eq(variables['VSCODE_COMPILE_ONLY'], 'false')) - dependsOn: - - Windows - - Windows32 - - Linux - - LinuxSnap - - LinuxArmhf - - LinuxArm64 - - LinuxAlpine - - LinuxWeb - - macOS - steps: - - template: sync-mooncake.yml - trigger: none pr: none @@ -161,3 +7,138 @@ schedules: branches: include: - master + +resources: + containers: + - container: vscode-x64 + image: vscodehub.azurecr.io/vscode-linux-build-agent:x64 + endpoint: VSCodeHub + - container: snapcraft + image: snapcore/snapcraft:stable + +stages: +- stage: Compile + jobs: + - job: Compile + pool: + vmImage: 'Ubuntu-16.04' + container: vscode-x64 + steps: + - template: product-compile.yml + +- stage: Windows + dependsOn: + - Compile + condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false')) + pool: + vmImage: VS2017-Win2016 + jobs: + - job: Windows + condition: and(succeeded(), eq(variables['VSCODE_BUILD_WIN32'], 'true')) + variables: + VSCODE_ARCH: x64 + steps: + - template: win32/product-build-win32.yml + + - job: Windows32 + condition: and(succeeded(), eq(variables['VSCODE_BUILD_WIN32_32BIT'], 'true')) + variables: + VSCODE_ARCH: ia32 + steps: + - template: win32/product-build-win32.yml + + - job: WindowsARM64 + condition: and(succeeded(), eq(variables['VSCODE_BUILD_WIN32_ARM64'], 'true')) + variables: + VSCODE_ARCH: arm64 + steps: + - template: win32/product-build-win32-arm64.yml + +- stage: Linux + dependsOn: + - Compile + condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false')) + pool: + vmImage: 'Ubuntu-16.04' + jobs: + - job: Linux + condition: and(succeeded(), eq(variables['VSCODE_BUILD_LINUX'], 'true')) + container: vscode-x64 + steps: + - template: linux/product-build-linux.yml + + - job: LinuxSnap + dependsOn: + - Linux + condition: and(succeeded(), eq(variables['VSCODE_BUILD_LINUX'], 'true')) + container: snapcraft + steps: + - template: linux/snap-build-linux.yml + + - job: LinuxArmhf + condition: and(succeeded(), eq(variables['VSCODE_BUILD_LINUX_ARMHF'], 'true')) + variables: + VSCODE_ARCH: armhf + steps: + - template: linux/product-build-linux-multiarch.yml + + - job: LinuxArm64 + condition: and(succeeded(), eq(variables['VSCODE_BUILD_LINUX_ARM64'], 'true')) + variables: + VSCODE_ARCH: arm64 + steps: + - template: linux/product-build-linux-multiarch.yml + + - job: LinuxAlpine + condition: and(succeeded(), eq(variables['VSCODE_BUILD_LINUX_ALPINE'], 'true')) + variables: + VSCODE_ARCH: alpine + steps: + - template: linux/product-build-linux-multiarch.yml + + - job: LinuxWeb + condition: and(succeeded(), eq(variables['VSCODE_BUILD_WEB'], 'true')) + variables: + VSCODE_ARCH: x64 + steps: + - template: web/product-build-web.yml + +- stage: macOS + dependsOn: + - Compile + condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false')) + pool: + vmImage: macOS-latest + jobs: + - job: macOS + condition: and(succeeded(), eq(variables['VSCODE_BUILD_MACOS'], 'true')) + steps: + - template: darwin/product-build-darwin.yml + +- stage: Mooncake + dependsOn: + - Windows + - Linux + - macOS + condition: and(succeededOrFailed(), eq(variables['VSCODE_COMPILE_ONLY'], 'false')) + pool: + vmImage: 'Ubuntu-16.04' + jobs: + - job: SyncMooncake + displayName: Sync Mooncake + steps: + - template: sync-mooncake.yml + +- stage: Publish + dependsOn: + - Windows + - Linux + - macOS + condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false'), or(eq(variables['VSCODE_RELEASE'], 'true'), and(or(eq(variables['VSCODE_QUALITY'], 'insider'), eq(variables['VSCODE_QUALITY'], 'exploration')), eq(variables['Build.Reason'], 'Schedule')))) + pool: + vmImage: 'Ubuntu-16.04' + jobs: + - job: BuildService + displayName: Build Service + steps: + - template: release.yml diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index db6524be03b..ab0dbb932c6 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -16,7 +16,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.13.0" + versionSpec: "12.14.1" condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true')) - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 diff --git a/build/azure-pipelines/publish-types/publish-types.yml b/build/azure-pipelines/publish-types/publish-types.yml index b73cd04a966..10b6aa4e16a 100644 --- a/build/azure-pipelines/publish-types/publish-types.yml +++ b/build/azure-pipelines/publish-types/publish-types.yml @@ -9,7 +9,7 @@ pr: none steps: - task: NodeTool@0 inputs: - versionSpec: "12.13.0" + versionSpec: "12.14.1" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/publish-types/update-types.ts b/build/azure-pipelines/publish-types/update-types.ts index c2677d446c6..9603726bebf 100644 --- a/build/azure-pipelines/publish-types/update-types.ts +++ b/build/azure-pipelines/publish-types/update-types.ts @@ -45,7 +45,7 @@ function repeat(str: string, times: number): string { } function convertTabsToSpaces(str: string): string { - return str.replace(/^\t+/gm, value => repeat(' ', value.length)); + return str.replace(/\t/gm, value => repeat(' ', value.length)); } function getNewFileContent(content: string, tag: string) { diff --git a/build/azure-pipelines/sync-mooncake.yml b/build/azure-pipelines/sync-mooncake.yml index 2641830a413..49dfc9ced80 100644 --- a/build/azure-pipelines/sync-mooncake.yml +++ b/build/azure-pipelines/sync-mooncake.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.13.0" + versionSpec: "12.14.1" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index 0c338203b4d..7f4907aa2d9 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -21,7 +21,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.13.0" + versionSpec: "12.14.1" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/win32/continuous-build-win32.yml b/build/azure-pipelines/win32/continuous-build-win32.yml index 026a162f510..8600377139c 100644 --- a/build/azure-pipelines/win32/continuous-build-win32.yml +++ b/build/azure-pipelines/win32/continuous-build-win32.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.13.0" + versionSpec: "12.14.1" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: @@ -57,7 +57,7 @@ steps: displayName: Run Unit Tests (Electron) - powershell: | - yarn test-browser --browser chromium --browser firefox + yarn test-browser --browser chromium --browser firefox --tfs "Browser Unit Tests" displayName: Run Unit Tests (Browser) - powershell: | diff --git a/build/azure-pipelines/win32/product-build-win32-arm64.yml b/build/azure-pipelines/win32/product-build-win32-arm64.yml index 01be34aa9a8..ecb50ad678e 100644 --- a/build/azure-pipelines/win32/product-build-win32-arm64.yml +++ b/build/azure-pipelines/win32/product-build-win32-arm64.yml @@ -21,7 +21,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.13.0" + versionSpec: "12.14.1" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index fb4f3052578..be80731a7ab 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -21,7 +21,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.13.0" + versionSpec: "12.14.1" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: @@ -115,7 +115,7 @@ steps: - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - exec { yarn test-browser --build --browser chromium --browser firefox } + exec { yarn test-browser --build --browser chromium --browser firefox --tfs "Browser Unit Tests" } displayName: Run unit tests (Browser) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) @@ -135,18 +135,18 @@ steps: - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" - $AppProductJson = Get-Content -Raw -Path "$AppRoot\resources\app\product.json" | ConvertFrom-Json - $AppNameShort = $AppProductJson.nameShort - exec { $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe"; $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)"; .\resources\server\test\test-remote-integration.bat } - displayName: Run remote integration tests (Electron) + exec { $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-web-win32-$(VSCODE_ARCH)"; .\resources\server\test\test-web-integration.bat --browser firefox } + displayName: Run integration tests (Browser) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - exec { $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-web-win32-$(VSCODE_ARCH)"; .\resources\server\test\test-web-integration.bat --browser firefox } - displayName: Run integration tests (Browser) + $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + $AppProductJson = Get-Content -Raw -Path "$AppRoot\resources\app\product.json" | ConvertFrom-Json + $AppNameShort = $AppProductJson.nameShort + exec { $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe"; $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)"; .\resources\server\test\test-remote-integration.bat } + displayName: Run remote integration tests (Electron) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - task: PublishPipelineArtifact@0 @@ -157,6 +157,13 @@ steps: continueOnError: true condition: failed() +- task: PublishTestResults@2 + displayName: Publish Tests Results + inputs: + testResultsFiles: '*-results.xml' + searchFolder: '$(Build.ArtifactStagingDirectory)/test-results' + condition: succeededOrFailed() + - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 inputs: ConnectedServiceName: 'ESRP CodeSign' diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index fd1d8ceeed9..4952ee1c26d 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -119,12 +119,12 @@ const copyrightFilter = [ '!resources/linux/snap/snapcraft.yaml', '!resources/linux/snap/electron-launch', '!resources/win32/bin/code.js', + '!resources/web/code-web.js', '!resources/completions/**', '!extensions/markdown-language-features/media/highlight.css', '!extensions/html-language-features/server/src/modes/typescript/*', '!extensions/*/server/bin/*', - '!src/vs/editor/test/node/classification/typescript-test.ts', - '!resources/serverless/code-web.js' + '!src/vs/editor/test/node/classification/typescript-test.ts' ]; const jsHygieneFilter = [ diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 1bf7d36a9f6..2c86b2808e0 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -43,6 +43,7 @@ const vscodeEntryPoints = _.flatten([ buildfile.entrypoint('vs/workbench/workbench.desktop.main'), buildfile.base, buildfile.workerExtensionHost, + buildfile.workerNotebook, buildfile.workbenchDesktop, buildfile.code ]); @@ -79,7 +80,7 @@ const vscodeResources = [ 'out-build/vs/code/electron-browser/sharedProcess/sharedProcess.js', 'out-build/vs/code/electron-sandbox/issue/issueReporter.js', 'out-build/vs/code/electron-sandbox/processExplorer/processExplorer.js', - 'out-build/vs/platform/auth/common/auth.css', + 'out-build/vs/code/electron-sandbox/proxy/auth.js', '!**/test/**' ]; diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 9cc40c4e1be..fe0deffc6d0 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -224,7 +224,6 @@ function packageLocalExtensionsStream(forWeb) { const extensionName = path.basename(extensionPath); return { name: extensionName, path: extensionPath, manifestPath: absoluteManifestPath }; }) - .filter(({ name }) => (name === 'vscode-web-playground' ? forWeb : true)) // package vscode-web-playground only for web .filter(({ name }) => excludedExtensions.indexOf(name) === -1) .filter(({ name }) => builtInExtensions.every(b => b.name !== name)) .filter(({ manifestPath }) => (forWeb ? isWebExtension(require(manifestPath)) : true))); diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 7e529f17cb8..dac71c81479 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -275,7 +275,6 @@ export function packageLocalExtensionsStream(forWeb: boolean): Stream { const extensionName = path.basename(extensionPath); return { name: extensionName, path: extensionPath, manifestPath: absoluteManifestPath }; }) - .filter(({ name }) => (name === 'vscode-web-playground' ? forWeb : true)) // package vscode-web-playground only for web .filter(({ name }) => excludedExtensions.indexOf(name) === -1) .filter(({ name }) => builtInExtensions.every(b => b.name !== name)) .filter(({ manifestPath }) => (forWeb ? isWebExtension(require(manifestPath)) : true)) diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 9bba404c243..ceef664b85f 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -246,10 +246,6 @@ "name": "vs/workbench/services/configurationResolver", "project": "vscode-workbench" }, - { - "name": "vs/workbench/services/crashReporter", - "project": "vscode-workbench" - }, { "name": "vs/workbench/services/dialogs", "project": "vscode-workbench" diff --git a/build/package.json b/build/package.json index 08f2bf30756..e185594554b 100644 --- a/build/package.json +++ b/build/package.json @@ -45,7 +45,7 @@ "minimist": "^1.2.3", "request": "^2.85.0", "terser": "4.3.8", - "typescript": "^4.0.0-dev.20200803", + "typescript": "^4.1.0-dev.20200824", "vsce": "1.48.0", "vscode-telemetry-extractor": "^1.6.0", "xml2js": "^0.4.17" diff --git a/build/win32/code.iss b/build/win32/code.iss index 3192b62f1e2..7f4b36e71aa 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -89,7 +89,7 @@ Source: "{#ProductJsonPath}"; DestDir: "{code:GetDestDir}\resources\app"; Flags: [Icons] Name: "{group}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; AppUserModelID: "{#AppUserId}" -Name: "{commondesktop}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: desktopicon; AppUserModelID: "{#AppUserId}" +Name: "{autodesktop}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: desktopicon; AppUserModelID: "{#AppUserId}" Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: quicklaunchicon; AppUserModelID: "{#AppUserId}" [Run] diff --git a/build/yarn.lock b/build/yarn.lock index 4aa0234e59f..01ebd186c4c 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -2535,10 +2535,10 @@ typescript@^3.0.1: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== -typescript@^4.0.0-dev.20200803: - version "4.0.0-dev.20200803" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.0-dev.20200803.tgz#ea8b0e9fb2ee3085598ff200c8568f04f4cbb2ba" - integrity sha512-f/jDkFqCs0gbUd5MCUijO9u3AOMx1x1HdRDDHSidlc6uPVEkRduxjeTFhIXbGutO7ivzv+aC2sxH+1FQwsyBcg== +typescript@^4.1.0-dev.20200824: + version "4.1.0-dev.20200824" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.0-dev.20200824.tgz#34c92d9b6e5124600658c0d4e9b8c125beaf577d" + integrity sha512-hTJfocmebnMKoqRw/xs3bL61z87XXtvOUwYtM7zaCX9mAvnfdo1x1bzQlLZAsvdzRIgAHPJQYbqYHKygWkDw6g== typical@^4.0.0: version "4.0.0" diff --git a/cgmanifest.json b/cgmanifest.json index cb9954628dd..e6e8ce8a5c1 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "e4745133a1d3745f066e068b8033c6a269b59caf" + "commitHash": "894fb9eb56c6cbda65e3c3ae9ada6d4cb5850cc9" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "78.0.3904.130" + "version": "83.0.4103.122" }, { "component": { @@ -48,11 +48,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "787378879acfb212ed4ff824bf9f767a24a5cb43a" + "commitHash": "9622fed3fb2cffcea9efff6c8cb4cc2def99d75d" } }, "isOnlyProductionDependency": true, - "version": "12.8.1" + "version": "12.14.1" }, { "component": { @@ -60,12 +60,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "5f93e889020d279d5a9cd1ecab080ab467312447" + "commitHash": "03c7a54dc534ce1867d4393b9b1a6989d4a7e005" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "7.3.2" + "version": "9.2.1" }, { "component": { diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 3cadb80fd33..477d530a0ed 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -54,7 +54,7 @@ "url": "vscode://schemas/keybindings" }, { - "fileMatch": "vscode://defaultsettings/defaultSettings.json", + "fileMatch": "vscode://defaultsettings/*/*.json", "url": "vscode://schemas/settings/default" }, { diff --git a/extensions/css-language-features/client/src/browser/cssClientMain.ts b/extensions/css-language-features/client/src/browser/cssClientMain.ts index 8b1d7205fcd..2a5e3e1f2c2 100644 --- a/extensions/css-language-features/client/src/browser/cssClientMain.ts +++ b/extensions/css-language-features/client/src/browser/cssClientMain.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionContext } from 'vscode'; +import { ExtensionContext, Uri } from 'vscode'; import { LanguageClientOptions } from 'vscode-languageclient'; import { startClient, LanguageClientConstructor } from '../cssClient'; import { LanguageClient } from 'vscode-languageclient/browser'; @@ -17,9 +17,9 @@ declare const TextDecoder: { // this method is called when vs code is activated export function activate(context: ExtensionContext) { - const serverMain = context.asAbsolutePath('server/dist/browser/cssServerMain.js'); + const serverMain = Uri.joinPath(context.extensionUri, 'server/dist/browser/cssServerMain.js'); try { - const worker = new Worker(serverMain); + const worker = new Worker(serverMain.toString()); const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { return new LanguageClient(id, name, clientOptions, worker); }; diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index ccbdba90a29..331af0883a9 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -10,7 +10,7 @@ "main": "./out/node/cssServerMain", "browser": "./dist/browser/cssServerMain", "dependencies": { - "vscode-css-languageservice": "^4.3.1", + "vscode-css-languageservice": "^4.3.3", "vscode-languageserver": "7.0.0-next.3", "vscode-uri": "^2.1.2" }, diff --git a/extensions/css-language-features/server/test/index.js b/extensions/css-language-features/server/test/index.js index 4e9960494a2..4ab853bd503 100644 --- a/extensions/css-language-features/server/test/index.js +++ b/extensions/css-language-features/server/test/index.js @@ -21,7 +21,7 @@ if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/css-language-features/server/yarn.lock b/extensions/css-language-features/server/yarn.lock index 3b9a29250f5..c93e187a941 100644 --- a/extensions/css-language-features/server/yarn.lock +++ b/extensions/css-language-features/server/yarn.lock @@ -696,10 +696,10 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -vscode-css-languageservice@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.3.1.tgz#a78755b28b8a0cbb1681121f0fa372860f34ef6b" - integrity sha512-Vdz2cyoTP2tLWikhFdouK8dAQ3gVhLPxsFkIscM30Quh6rd/YejTeZEYC/W+b0iKumHYebDeo1GUFbf0ptySRw== +vscode-css-languageservice@^4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.3.3.tgz#fcb8c7442ae7bb8fbe6ff1d3a514be8248bfb52f" + integrity sha512-b2b+0oHvPmBHygDtOXX3xBvpQCa6eIQSvXnGDNSDmIC1894ZTJ2yX10vjplOO/PvV7mwhyvGPwHyY4X2HGxtKw== dependencies: vscode-languageserver-textdocument "^1.0.1" vscode-languageserver-types "3.16.0-next.2" diff --git a/extensions/debug-auto-launch/.vscode/launch.json b/extensions/debug-auto-launch/.vscode/launch.json new file mode 100644 index 00000000000..1c3d9e9661b --- /dev/null +++ b/extensions/debug-auto-launch/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Extension", + "type": "extensionHost", + "request": "launch", + "skipFiles": ["/**"], + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js", + ], + } + ] +} diff --git a/extensions/debug-auto-launch/package.json b/extensions/debug-auto-launch/package.json index 07568c77081..3cb11ef1844 100644 --- a/extensions/debug-auto-launch/package.json +++ b/extensions/debug-auto-launch/package.json @@ -35,6 +35,12 @@ ], "description": "%debug.node.autoAttach.description%", "default": "disabled" + }, + "debug.javascript.usePreviewAutoAttach": { + "scope": "window", + "type": "boolean", + "default": true, + "description": "%debug.javascript.usePreviewAutoAttach%" } } }, diff --git a/extensions/debug-auto-launch/package.nls.json b/extensions/debug-auto-launch/package.nls.json index 030ac5a20a9..1179563a6c5 100644 --- a/extensions/debug-auto-launch/package.nls.json +++ b/extensions/debug-auto-launch/package.nls.json @@ -3,9 +3,10 @@ "description": "Helper for auto-attach feature when node-debug extensions are not active.", "debug.node.autoAttach.description": "Automatically attach node debugger when node.js was launched in debug mode from integrated terminal.", + "debug.javascript.usePreviewAutoAttach": "Whether to use the preview debugger's version of auto attach.", "debug.node.autoAttach.disabled.description": "Auto attach is disabled and not shown in status bar.", "debug.node.autoAttach.on.description": "Auto attach is active.", "debug.node.autoAttach.off.description": "Auto attach is inactive.", "toggle.auto.attach": "Toggle Auto Attach" -} \ No newline at end of file +} diff --git a/extensions/debug-auto-launch/src/extension.ts b/extensions/debug-auto-launch/src/extension.ts index ef58db6a5ea..b70fb3dedbe 100644 --- a/extensions/debug-auto-launch/src/extension.ts +++ b/extensions/debug-auto-launch/src/extension.ts @@ -13,11 +13,11 @@ const OFF_TEXT = localize('status.text.auto.attach.off', 'Auto Attach: Off'); const TOGGLE_COMMAND = 'extension.node-debug.toggleAutoAttach'; const JS_DEBUG_SETTINGS = 'debug.javascript'; -const JS_DEBUG_USEPREVIEW = 'usePreview'; +const JS_DEBUG_USEPREVIEWAA = 'usePreviewAutoAttach'; const JS_DEBUG_IPC_KEY = 'jsDebugIpcState'; const NODE_DEBUG_SETTINGS = 'debug.node'; -const NODE_DEBUG_USEV3 = 'useV3'; const AUTO_ATTACH_SETTING = 'autoAttach'; +const LAST_STATE_STORAGE_KEY = 'lastState'; type AUTO_ATTACH_VALUES = 'disabled' | 'on' | 'off'; @@ -29,33 +29,36 @@ const enum State { } // on activation this feature is always disabled... -let currentState = Promise.resolve({ state: State.Disabled, transitionData: null as unknown }); +let currentState: Promise<{ context: vscode.ExtensionContext, state: State; transitionData: unknown }>; let statusItem: vscode.StatusBarItem | undefined; // and there is no status bar item export function activate(context: vscode.ExtensionContext): void { + const previousState = context.workspaceState.get(LAST_STATE_STORAGE_KEY, State.Disabled); + currentState = Promise.resolve(transitions[previousState].onActivate?.(context, readCurrentState())) + .then(() => ({ context, state: State.Disabled, transitionData: null })); + context.subscriptions.push(vscode.commands.registerCommand(TOGGLE_COMMAND, toggleAutoAttachSetting)); // settings that can result in the "state" being changed--on/off/disable or useV3 toggles const effectualConfigurationSettings = [ `${NODE_DEBUG_SETTINGS}.${AUTO_ATTACH_SETTING}`, - `${NODE_DEBUG_SETTINGS}.${NODE_DEBUG_USEV3}`, - `${JS_DEBUG_SETTINGS}.${JS_DEBUG_USEPREVIEW}`, + `${JS_DEBUG_SETTINGS}.${JS_DEBUG_USEPREVIEWAA}`, ]; context.subscriptions.push( vscode.workspace.onDidChangeConfiguration((e) => { if (effectualConfigurationSettings.some(setting => e.affectsConfiguration(setting))) { - updateAutoAttach(context); + updateAutoAttach(); } }) ); - updateAutoAttach(context); + updateAutoAttach(); } export async function deactivate(): Promise { - const { state, transitionData } = await currentState; - await transitions[state].exit?.(transitionData); + const { context, state, transitionData } = await currentState; + await transitions[state].exit?.(context, transitionData); } function toggleAutoAttachSetting() { @@ -88,6 +91,11 @@ function toggleAutoAttachSetting() { } } +function autoAttachWithJsDebug() { + const jsDebugConfig = vscode.workspace.getConfiguration(JS_DEBUG_SETTINGS); + return jsDebugConfig.get(JS_DEBUG_USEPREVIEWAA, true); +} + function readCurrentState(): State { const nodeConfig = vscode.workspace.getConfiguration(NODE_DEBUG_SETTINGS); const autoAttachState = nodeConfig.get(AUTO_ATTACH_SETTING); @@ -95,11 +103,7 @@ function readCurrentState(): State { case 'off': return State.Off; case 'on': - // todo: reenable after resolving https://github.com/microsoft/vscode/issues/102057 - // const jsDebugConfig = vscode.workspace.getConfiguration(JS_DEBUG_SETTINGS); - // const useV3 = nodeConfig.get(NODE_DEBUG_USEV3) || jsDebugConfig.get(JS_DEBUG_USEPREVIEW); - // return useV3 ? State.OnWithJsDebug : State.OnWithNodeDebug; - return State.OnWithNodeDebug; + return autoAttachWithJsDebug() ? State.OnWithJsDebug : State.OnWithNodeDebug; case 'disabled': default: return State.Disabled; @@ -126,37 +130,44 @@ function ensureStatusBarExists(context: vscode.ExtensionContext) { return statusItem; } +async function clearJsDebugAttachState(context: vscode.ExtensionContext) { + await context.workspaceState.update(JS_DEBUG_IPC_KEY, undefined); + await vscode.commands.executeCommand('extension.js-debug.clearAutoAttachVariables'); +} + interface CachedIpcState { ipcAddress: string; jsDebugPath: string; } interface StateTransition { - exit?(stateData: StateData): Promise | void; + onActivate?(context: vscode.ExtensionContext, currentState: State): Promise; + exit?(context: vscode.ExtensionContext, stateData: StateData): Promise | void; enter?(context: vscode.ExtensionContext): Promise | StateData; } +const makeTransition = (tsn: StateTransition) => tsn; // helper to apply generic type + /** * Map of logic that happens when auto attach states are entered and exited. * All state transitions are queued and run in order; promises are awaited. */ const transitions: { [S in State]: StateTransition } = { - [State.Disabled]: { + [State.Disabled]: makeTransition({ async enter(context) { statusItem?.hide(); - await context.workspaceState.update(JS_DEBUG_IPC_KEY, undefined); - await vscode.commands.executeCommand('extension.js-debug.clearAutoAttachVariables'); + await clearJsDebugAttachState(context); }, - }, + }), - [State.Off]: { + [State.Off]: makeTransition({ enter(context) { const statusItem = ensureStatusBarExists(context); statusItem.text = OFF_TEXT; }, - }, + }), - [State.OnWithNodeDebug]: { + [State.OnWithNodeDebug]: makeTransition({ async enter(context) { const statusItem = ensureStatusBarExists(context); const vscode_pid = process.env['VSCODE_PID']; @@ -168,21 +179,37 @@ const transitions: { [S in State]: StateTransition } = { async exit() { await vscode.commands.executeCommand('extension.node-debug.stopAutoAttach'); }, - }, + }), - [State.OnWithJsDebug]: { + [State.OnWithJsDebug]: makeTransition({ async enter(context) { const ipcAddress = await getIpcAddress(context); - const server = await new Promise((resolve, reject) => { + if (!ipcAddress) { + return null; + } + + const server = await new Promise((resolve, reject) => { const s = createServer((socket) => { let data: Buffer[] = []; - socket.on('data', (chunk) => data.push(chunk)); - socket.on('end', () => - vscode.commands.executeCommand( - 'extension.js-debug.autoAttachToProcess', - JSON.parse(Buffer.concat(data).toString()) - ) - ); + socket.on('data', async (chunk) => { + if (chunk[chunk.length - 1] !== 0) { // terminated with NUL byte + data.push(chunk); + return; + } + + data.push(chunk.slice(0, -1)); + + try { + await vscode.commands.executeCommand( + 'extension.js-debug.autoAttachToProcess', + JSON.parse(Buffer.concat(data).toString()) + ); + socket.write(Buffer.from([0])); + } catch (err) { + socket.write(Buffer.from([1])); + console.error(err); + } + }); }) .on('error', reject) .listen(ipcAddress, () => resolve(s)); @@ -190,33 +217,47 @@ const transitions: { [S in State]: StateTransition } = { const statusItem = ensureStatusBarExists(context); statusItem.text = ON_TEXT; - return server; + return server || null; }, - async exit(server: Server) { + async exit(context, server) { // we don't need to clear the environment variables--the bootloader will // no-op if the debug server is closed. This prevents having to reload // terminals if users want to turn it back on. - await new Promise((resolve) => server.close(resolve)); + if (server) { + await new Promise((resolve) => server.close(resolve)); + } + + // but if they toggled auto attach use js-debug off, go ahead and do so + if (!autoAttachWithJsDebug()) { + await clearJsDebugAttachState(context); + } }, - }, + + async onActivate(context, currentState) { + if (currentState === State.OnWithNodeDebug || currentState === State.Disabled) { + await clearJsDebugAttachState(context); + } + } + }), }; /** * Updates the auto attach feature based on the user or workspace setting */ -function updateAutoAttach(context: vscode.ExtensionContext) { +function updateAutoAttach() { const newState = readCurrentState(); - currentState = currentState.then(async ({ state: oldState, transitionData }) => { + currentState = currentState.then(async ({ context, state: oldState, transitionData }) => { if (newState === oldState) { - return { state: oldState, transitionData }; + return { context, state: oldState, transitionData }; } - await transitions[oldState].exit?.(transitionData); + await transitions[oldState].exit?.(context, transitionData); const newData = await transitions[newState].enter?.(context); + await context.workspaceState.update(LAST_STATE_STORAGE_KEY, newState); - return { state: newState, transitionData: newData }; + return { context, state: newState, transitionData: newData }; }); } @@ -244,8 +285,11 @@ async function getIpcAddress(context: vscode.ExtensionContext) { const result = await vscode.commands.executeCommand<{ ipcAddress: string; }>( 'extension.js-debug.setAutoAttachVariables' ); + if (!result) { + return; + } - const ipcAddress = result!.ipcAddress; + const ipcAddress = result.ipcAddress; await context.workspaceState.update(JS_DEBUG_IPC_KEY, { ipcAddress, jsDebugPath }); return ipcAddress; } diff --git a/extensions/docker/package.json b/extensions/docker/package.json index 3af7727a6b4..a1cc782d213 100644 --- a/extensions/docker/package.json +++ b/extensions/docker/package.json @@ -15,7 +15,7 @@ "extensions": [ ".dockerfile", ".containerfile" ], "filenames": [ "Dockerfile", "Containerfile" ], "filenamePatterns": [ "Dockerfile.*", "Containerfile.*" ], - "aliases": [ "Dockerfile", "Containerfile" ], + "aliases": [ "Docker", "Dockerfile", "Containerfile" ], "configuration": "./language-configuration.json" }], "grammars": [{ diff --git a/extensions/emmet/extension.webpack.config.js b/extensions/emmet/extension.webpack.config.js index 96ada4c23a7..bfac2b59f47 100644 --- a/extensions/emmet/extension.webpack.config.js +++ b/extensions/emmet/extension.webpack.config.js @@ -21,6 +21,6 @@ module.exports = withDefaults({ filename: 'emmetNodeMain.js' }, externals: { - 'vscode-emmet-helper2': 'commonjs vscode-emmet-helper2', + 'vscode-emmet-helper': 'commonjs vscode-emmet-helper', }, }); diff --git a/extensions/emmet/package.json b/extensions/emmet/package.json index aadbba7edea..d02c19a78ac 100644 --- a/extensions/emmet/package.json +++ b/extensions/emmet/package.json @@ -422,7 +422,7 @@ "scripts": { "watch": "gulp watch-extension:emmet", "compile": "gulp compile-extension:emmet", - "deps": "yarn add vscode-emmet-helper2" + "deps": "yarn add vscode-emmet-helper" }, "devDependencies": { "@types/node": "^12.11.7", @@ -435,7 +435,7 @@ "@emmetio/html-matcher": "^0.3.3", "@emmetio/math-expression": "^0.1.1", "image-size": "^0.5.2", - "vscode-emmet-helper2": "^2.0.0-next.0", + "vscode-emmet-helper": "^2.0.0", "vscode-html-languageservice": "^3.0.3" } } diff --git a/extensions/emmet/src/abbreviationActions.ts b/extensions/emmet/src/abbreviationActions.ts index 367f4de2e9d..aa1d2563dbf 100644 --- a/extensions/emmet/src/abbreviationActions.ts +++ b/extensions/emmet/src/abbreviationActions.ts @@ -606,6 +606,25 @@ function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrList: Ex return Promise.resolve(false); } +/* +* Walks the tree rooted at root and apply function fn on each node. +* if fn return false at any node, the further processing of tree is stopped. +*/ +function walk(root: any, fn: ((node: any) => boolean)): boolean { + let ctx = root; + while (ctx) { + + let next = ctx.next; + if (fn(ctx) === false || walk(ctx.firstChild, fn) === false) { + return false; + } + + ctx = next; + } + + return true; +} + /** * Expands abbreviation as detailed in given input. */ @@ -648,6 +667,18 @@ function expandAbbr(input: ExpandAbbreviationInput): string | undefined { wrappingNode.value = '\n\t' + wrappingNode.value + '\n'; } } + + // Below fixes https://github.com/microsoft/vscode/issues/78219 + // walk the tree and remove tags for empty values + walk(parsedAbbr, node => { + if (node.name !== null && node.value === '' && !node.isSelfClosing && node.children.length === 0) { + node.name = ''; + node.value = '\n'; + } + + return true; + }); + expandedText = helper.expandAbbreviation(parsedAbbr, expandOptions); // All $anyword would have been escaped by the emmet helper. // Remove the escaping backslash from $TM_SELECTED_TEXT so that VS Code Snippet controller can treat it as a variable diff --git a/extensions/emmet/src/test/index.ts b/extensions/emmet/src/test/index.ts index f3aeb0fefc1..3aeda9dfa42 100644 --- a/extensions/emmet/src/test/index.ts +++ b/extensions/emmet/src/test/index.ts @@ -6,21 +6,31 @@ const path = require('path'); const testRunner = require('vscode/lib/testrunner'); -const suite = 'Integration Emmet Tests'; - const options: any = { ui: 'tdd', useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), timeout: 60000 }; +// These integration tests is being run in multiple environments (electron, web, remote) +// so we need to set the suite name based on the environment as the suite name is used +// for the test results file name +let suite = ''; +if (process.env.VSCODE_BROWSER) { + suite = `${process.env.VSCODE_BROWSER} Browser Integration Emmet Tests`; +} else if (process.env.REMOTE_VSCODE) { + suite = 'Remote Integration Emmet Tests'; +} else { + suite = 'Integration Emmet Tests'; +} + if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/emmet/src/util.ts b/extensions/emmet/src/util.ts index 0dc2a0e201d..8ef33e97a83 100644 --- a/extensions/emmet/src/util.ts +++ b/extensions/emmet/src/util.ts @@ -8,7 +8,7 @@ import parse from '@emmetio/html-matcher'; import parseStylesheet from '@emmetio/css-parser'; import { Node, HtmlNode, CssToken, Property, Rule, Stylesheet } from 'EmmetNode'; import { DocumentStreamReader } from './bufferStream'; -import * as EmmetHelper from 'vscode-emmet-helper2'; +import * as EmmetHelper from 'vscode-emmet-helper'; import { TextDocument as LSTextDocument } from 'vscode-html-languageservice'; let _emmetHelper: typeof EmmetHelper; @@ -26,7 +26,7 @@ export function getEmmetHelper() { // Lazy load vscode-emmet-helper instead of importing it // directly to reduce the start-up time of the extension if (!_emmetHelper) { - _emmetHelper = require('vscode-emmet-helper2'); + _emmetHelper = require('vscode-emmet-helper'); } updateEmmetExtensionsPath(); return _emmetHelper; diff --git a/extensions/emmet/yarn.lock b/extensions/emmet/yarn.lock index 06a8845658c..cd26963d81b 100644 --- a/extensions/emmet/yarn.lock +++ b/extensions/emmet/yarn.lock @@ -2469,10 +2469,10 @@ vinyl@~2.0.1: remove-trailing-separator "^1.0.1" replace-ext "^1.0.0" -vscode-emmet-helper2@^2.0.0-next.0: - version "2.0.0-next.0" - resolved "https://registry.yarnpkg.com/vscode-emmet-helper2/-/vscode-emmet-helper2-2.0.0-next.0.tgz#86eb4c2e581a577e7eb56a51f662e72fb1c63b47" - integrity sha512-ccm6Fb5dkbdEDNLIAebWwVcb8X3AXZDsACLi4KYdCxyFSMV+pOoNokBf4rsu+rSYWNe+fMqxjXZs9z0G2CxPGg== +vscode-emmet-helper@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/vscode-emmet-helper/-/vscode-emmet-helper-2.0.0.tgz#0057ec2d4af8ac83b1f7937383714ffdc56fcc07" + integrity sha512-ytR+Ajxs6zeYI0b4bPsl+nPU8xm852piJUtIwO1ajp1Pw7lwn3VeR+f4ynmxOl9IjfOdF2kW9T/qIkeFbKLwYw== dependencies: "@emmetio/extract-abbreviation" "^0.2.0" jsonc-parser "^2.3.0" diff --git a/extensions/git/package.json b/extensions/git/package.json index 748868778dd..1e577006c1f 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -754,13 +754,23 @@ "when": "scmProvider == git" }, { - "command": "git.checkout", - "group": "1_header", + "command": "git.pull", + "group": "1_header@1", + "when": "scmProvider == git" + }, + { + "command": "git.push", + "group": "1_header@2", "when": "scmProvider == git" }, { "command": "git.clone", - "group": "1_header", + "group": "1_header@3", + "when": "scmProvider == git" + }, + { + "command": "git.checkout", + "group": "1_header@4", "when": "scmProvider == git" }, { @@ -1915,7 +1925,11 @@ "[git-commit]": { "editor.rulers": [ 72 - ] + ], + "workbench.editor.restoreViewState": false + }, + "[git-rebase]": { + "workbench.editor.restoreViewState": false } }, "viewsWelcome": [ diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 0e8ea1c649e..c412586f43c 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -107,7 +107,7 @@ "config.smartCommitChanges.all": "Automatically stage all changes.", "config.smartCommitChanges.tracked": "Automatically stage tracked changes only.", "config.suggestSmartCommit": "Suggests to enable smart commit (commit all changes when there are no staged changes).", - "config.enableCommitSigning": "Enables commit signing with GPG.", + "config.enableCommitSigning": "Enables commit signing with GPG or X.509.", "config.discardAllScope": "Controls what changes are discarded by the `Discard all changes` command. `all` discards all changes. `tracked` discards only tracked files. `prompt` shows a prompt dialog every time the action is run.", "config.decorations.enabled": "Controls whether Git contributes colors and badges to the explorer and the open editors view.", "config.enableStatusBarSync": "Controls whether the Git Sync command appears in the status bar.", diff --git a/extensions/git/src/test/index.ts b/extensions/git/src/test/index.ts index 747c4562e8a..8773f772e62 100644 --- a/extensions/git/src/test/index.ts +++ b/extensions/git/src/test/index.ts @@ -6,21 +6,31 @@ const path = require('path'); const testRunner = require('vscode/lib/testrunner'); -const suite = 'Integration Git Tests'; - const options: any = { ui: 'tdd', useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), timeout: 60000 }; +// These integration tests is being run in multiple environments (electron, web, remote) +// so we need to set the suite name based on the environment as the suite name is used +// for the test results file name +let suite = ''; +if (process.env.VSCODE_BROWSER) { + suite = `${process.env.VSCODE_BROWSER} Browser Integration Git Tests`; +} else if (process.env.REMOTE_VSCODE) { + suite = 'Remote Integration Git Tests'; +} else { + suite = 'Integration Git Tests'; +} + if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 5600b6f0e0c..94857f8e210 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -232,7 +232,7 @@ export class GitTimelineProvider implements TimelineProvider { private onRepositoryStatusChanged(_repo: Repository) { // console.log(`GitTimelineProvider.onRepositoryStatusChanged`); - // This is crappy, but for now just save the last time a status was run and use that as the timestamp for staged items + // This is less than ideal, but for now just save the last time a status was run and use that as the timestamp for staged items this.repoStatusDate = new Date(); this.fireChanged(); diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index c4189e6b7eb..b74a91843a3 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -11,8 +11,12 @@ "categories": [ "Other" ], + "extensionKind": [ + "ui", + "workspace", + "web" + ], "activationEvents": [ - "*", "onAuthenticationRequest:github" ], "contributes": { @@ -29,7 +33,13 @@ "when": "false" } ] - } + }, + "authentication": [ + { + "label": "GitHub", + "id": "github" + } + ] }, "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", "main": "./out/extension.js", diff --git a/extensions/groovy/package.json b/extensions/groovy/package.json index 55da6a97fc8..ab2ef0da7ba 100644 --- a/extensions/groovy/package.json +++ b/extensions/groovy/package.json @@ -13,8 +13,8 @@ "languages": [{ "id": "groovy", "aliases": ["Groovy", "groovy"], - "extensions": [".groovy", ".gvy", ".gradle"], - "filenames": [ "Jenkinsfile" ], + "extensions": [".groovy", ".gvy", ".gradle", ".jenkinsfile"], + "filenamePatterns": ["Jenkinsfile.*"], "firstLine": "^#!.*\\bgroovy\\b", "configuration": "./language-configuration.json" }], diff --git a/extensions/html-language-features/client/src/browser/htmlClientMain.ts b/extensions/html-language-features/client/src/browser/htmlClientMain.ts index 1623bea9e20..425dfcd6609 100644 --- a/extensions/html-language-features/client/src/browser/htmlClientMain.ts +++ b/extensions/html-language-features/client/src/browser/htmlClientMain.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionContext } from 'vscode'; +import { ExtensionContext, Uri } from 'vscode'; import { LanguageClientOptions } from 'vscode-languageclient'; import { startClient, LanguageClientConstructor } from '../htmlClient'; import { LanguageClient } from 'vscode-languageclient/browser'; @@ -17,9 +17,9 @@ declare const TextDecoder: { // this method is called when vs code is activated export function activate(context: ExtensionContext) { - const serverMain = context.asAbsolutePath('server/dist/browser/htmlServerMain.js'); + const serverMain = Uri.joinPath(context.extensionUri, 'server/dist/browser/htmlServerMain.js'); try { - const worker = new Worker(serverMain); + const worker = new Worker(serverMain.toString()); const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { return new LanguageClient(id, name, clientOptions, worker); }; diff --git a/extensions/html-language-features/client/src/htmlClient.ts b/extensions/html-language-features/client/src/htmlClient.ts index e818d1a2d02..6917b56950b 100644 --- a/extensions/html-language-features/client/src/htmlClient.ts +++ b/extensions/html-language-features/client/src/htmlClient.ts @@ -28,7 +28,7 @@ namespace TagCloseRequest { export const type: RequestType = new RequestType('html/tag'); } namespace OnTypeRenameRequest { - export const type: RequestType = new RequestType('html/onTypeRename'); + export const type: RequestType = new RequestType('html/onTypeRename'); } // experimental: semantic tokens @@ -172,9 +172,14 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua disposable = languages.registerOnTypeRenameProvider(documentSelector, { async provideOnTypeRenameRanges(document, position) { const param = client.code2ProtocolConverter.asTextDocumentPositionParams(document, position); - const response = await client.sendRequest(OnTypeRenameRequest.type, param); - - return response || []; + return client.sendRequest(OnTypeRenameRequest.type, param).then(response => { + if (response) { + return { + ranges: response.map(r => client.protocol2CodeConverter.asRange(r)) + }; + } + return undefined; + }); } }); toDispose.push(disposable); diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index eadd61b33d0..644540a20fd 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -9,8 +9,8 @@ }, "main": "./out/node/htmlServerMain", "dependencies": { - "vscode-css-languageservice": "^4.3.1", - "vscode-html-languageservice": "^3.1.0", + "vscode-css-languageservice": "^4.3.3", + "vscode-html-languageservice": "^3.1.1", "vscode-languageserver": "7.0.0-next.3", "vscode-nls": "^4.1.2", "vscode-uri": "^2.1.2" diff --git a/extensions/html-language-features/server/src/test/folding.test.ts b/extensions/html-language-features/server/src/test/folding.test.ts index 272a81463fd..693a74420f0 100644 --- a/extensions/html-language-features/server/src/test/folding.test.ts +++ b/extensions/html-language-features/server/src/test/folding.test.ts @@ -71,7 +71,7 @@ suite('HTML Folding', async () => { /*13*/'', /*14*/'', ]; - await assertRanges(input, [r(0, 13), r(1, 12), r(2, 6), r(3, 6), r(8, 11), r(9, 11)]); + await assertRanges(input, [r(0, 13), r(1, 12), r(2, 6), r(3, 6), r(8, 11), r(9, 11), r(9, 11)]); }); test('Embedded JavaScript - incomplete', async () => { diff --git a/extensions/html-language-features/server/test/index.js b/extensions/html-language-features/server/test/index.js index d177599c624..5f7aa21e58a 100644 --- a/extensions/html-language-features/server/test/index.js +++ b/extensions/html-language-features/server/test/index.js @@ -21,7 +21,7 @@ if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/html-language-features/server/yarn.lock b/extensions/html-language-features/server/yarn.lock index dc001b1ada8..4a64d9a49e1 100644 --- a/extensions/html-language-features/server/yarn.lock +++ b/extensions/html-language-features/server/yarn.lock @@ -721,20 +721,20 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -vscode-css-languageservice@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.3.1.tgz#a78755b28b8a0cbb1681121f0fa372860f34ef6b" - integrity sha512-Vdz2cyoTP2tLWikhFdouK8dAQ3gVhLPxsFkIscM30Quh6rd/YejTeZEYC/W+b0iKumHYebDeo1GUFbf0ptySRw== +vscode-css-languageservice@^4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.3.3.tgz#fcb8c7442ae7bb8fbe6ff1d3a514be8248bfb52f" + integrity sha512-b2b+0oHvPmBHygDtOXX3xBvpQCa6eIQSvXnGDNSDmIC1894ZTJ2yX10vjplOO/PvV7mwhyvGPwHyY4X2HGxtKw== dependencies: vscode-languageserver-textdocument "^1.0.1" vscode-languageserver-types "3.16.0-next.2" vscode-nls "^4.1.2" vscode-uri "^2.1.2" -vscode-html-languageservice@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-3.1.0.tgz#265b53bda595e6947b16b0fb8c604e1e58685393" - integrity sha512-QAyRHI98bbEIBCqTzZVA0VblGU40na0txggongw5ZgTj9UVsVk5XbLT16O9OTcbqBGSqn0oWmFDNjK/XGIDcqg== +vscode-html-languageservice@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-3.1.1.tgz#ae9fa52b5415f332d73c65c46d4e2ed87ef81c8c" + integrity sha512-SKrMnoZtI0q+NJEjNJRgzvQeLih0obRvGLh1r5ysd5YGxhNSZbQgqeEgK4jsGjAp3nr4QWIzop3WjP/30BLsPw== dependencies: vscode-languageserver-textdocument "^1.0.1" vscode-languageserver-types "3.16.0-next.2" diff --git a/extensions/html/package.json b/extensions/html/package.json index 065eb26669b..6b1eac2d702 100644 --- a/extensions/html/package.json +++ b/extensions/html/package.json @@ -20,6 +20,7 @@ ".htm", ".shtml", ".xhtml", + ".xht", ".mdoc", ".jsp", ".asp", diff --git a/extensions/image-preview/src/extension.ts b/extensions/image-preview/src/extension.ts index 552b32d39b6..10722360dd5 100644 --- a/extensions/image-preview/src/extension.ts +++ b/extensions/image-preview/src/extension.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { BinarySizeStatusBarEntry } from './binarySizeStatusBarEntry'; import { PreviewManager } from './preview'; import { SizeStatusBarEntry } from './sizeStatusBarEntry'; -import { BinarySizeStatusBarEntry } from './binarySizeStatusBarEntry'; import { ZoomStatusBarEntry } from './zoomStatusBarEntry'; export function activate(context: vscode.ExtensionContext) { diff --git a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json b/extensions/javascript/syntaxes/JavaScript.tmLanguage.json index 60f6ce87547..a3daae76943 100644 --- a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json +++ b/extensions/javascript/syntaxes/JavaScript.tmLanguage.json @@ -5737,4 +5737,4 @@ "match": "\\S+" } } -} +} \ No newline at end of file diff --git a/extensions/json-language-features/client/src/browser/jsonClientMain.ts b/extensions/json-language-features/client/src/browser/jsonClientMain.ts index 488343f42ca..6389dafb5ba 100644 --- a/extensions/json-language-features/client/src/browser/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/browser/jsonClientMain.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionContext } from 'vscode'; +import { ExtensionContext, Uri } from 'vscode'; import { LanguageClientOptions } from 'vscode-languageclient'; import { startClient, LanguageClientConstructor } from '../jsonClient'; import { LanguageClient } from 'vscode-languageclient/browser'; @@ -17,9 +17,9 @@ declare function fetch(uri: string, options: any): any; // this method is called when vs code is activated export function activate(context: ExtensionContext) { - const serverMain = context.asAbsolutePath('server/dist/browser/jsonServerMain.js'); + const serverMain = Uri.joinPath(context.extensionUri, 'server/dist/browser/jsonServerMain.js'); try { - const worker = new Worker(serverMain); + const worker = new Worker(serverMain.toString()); const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { return new LanguageClient(id, name, clientOptions, worker); }; diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index e3e507c7b6c..ddf75c333e6 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -14,7 +14,7 @@ "dependencies": { "jsonc-parser": "^2.2.1", "request-light": "^0.3.0", - "vscode-json-languageservice": "^3.8.0", + "vscode-json-languageservice": "^3.8.3", "vscode-languageserver": "7.0.0-next.3", "vscode-uri": "^2.1.2" }, diff --git a/extensions/json-language-features/server/yarn.lock b/extensions/json-language-features/server/yarn.lock index 281f8276eb3..ac7354a958e 100644 --- a/extensions/json-language-features/server/yarn.lock +++ b/extensions/json-language-features/server/yarn.lock @@ -80,10 +80,10 @@ request-light@^0.3.0: https-proxy-agent "^2.2.4" vscode-nls "^4.1.1" -vscode-json-languageservice@^3.8.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-3.8.0.tgz#c7e7283f993e3db39fa5501407b023ada6fd3ae3" - integrity sha512-sYz5JElJMIlPoqhrRfG3VKnDjnPinLdblIiEVsJgTz1kj2hWD2q5BSbo+evH/5/jKDXDLfA8kb0lHC4vd5g5zg== +vscode-json-languageservice@^3.8.3: + version "3.8.3" + resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-3.8.3.tgz#fae5e7bdda2b6ec4f64588c571df40b6bfcb09b5" + integrity sha512-8yPag/NQHCuTthahyaTtzK0DHT0FKM/xBU0mFBQ8nMo8C1i2P+FCyIVqICoNoHkRI2BTGlXKomPUpsqjSz0TnQ== dependencies: jsonc-parser "^2.2.1" vscode-languageserver-textdocument "^1.0.1" diff --git a/extensions/markdown-basics/snippets/markdown.code-snippets b/extensions/markdown-basics/snippets/markdown.code-snippets index 6ee831ae4a5..b07f984138c 100644 --- a/extensions/markdown-basics/snippets/markdown.code-snippets +++ b/extensions/markdown-basics/snippets/markdown.code-snippets @@ -14,43 +14,54 @@ "body": "> ${1:${TM_SELECTED_TEXT}}", "description": "Insert quoted text" }, - "Insert code": { + "Insert inline code": { "prefix": "code", "body": "`${1:${TM_SELECTED_TEXT}}`$0", - "description": "Insert code" + "description": "Insert inline code" }, "Insert fenced code block": { "prefix": "fenced codeblock", - "body": [ - "```${1:language}", - "${TM_SELECTED_TEXT}$0", - "```" - ], + "body": ["```${1:language}", "${TM_SELECTED_TEXT}$0", "```"], "description": "Insert fenced code block" }, - "Insert heading": { - "prefix": "heading", + "Insert heading level 1": { + "prefix": "heading1", "body": "# ${1:${TM_SELECTED_TEXT}}", - "description": "Insert heading" + "description": "Insert heading level 1" + }, + "Insert heading level 2": { + "prefix": "heading2", + "body": "## ${1:${TM_SELECTED_TEXT}}", + "description": "Insert heading level 2" + }, + "Insert heading level 3": { + "prefix": "heading3", + "body": "### ${1:${TM_SELECTED_TEXT}}", + "description": "Insert heading level 3" + }, + "Insert heading level 4": { + "prefix": "heading4", + "body": "#### ${1:${TM_SELECTED_TEXT}}", + "description": "Insert heading level 4" + }, + "Insert heading level 5": { + "prefix": "heading5", + "body": "##### ${1:${TM_SELECTED_TEXT}}", + "description": "Insert heading level 5" + }, + "Insert heading level 6": { + "prefix": "heading6", + "body": "###### ${1:${TM_SELECTED_TEXT}}", + "description": "Insert heading level 6" }, "Insert unordered list": { "prefix": "unordered list", - "body": [ - "- ${1:first}", - "- ${2:second}", - "- ${3:third}", - "$0" - ], + "body": ["- ${1:first}", "- ${2:second}", "- ${3:third}", "$0"], "description": "Insert unordered list" }, "Insert ordered list": { "prefix": "ordered list", - "body": [ - "1. ${1:first}", - "2. ${2:second}", - "3. ${3:third}", - "$0" - ], + "body": ["1. ${1:first}", "2. ${2:second}", "3. ${3:third}", "$0"], "description": "Insert ordered list" }, "Insert horizontal rule": { @@ -67,5 +78,10 @@ "prefix": "image", "body": "![${TM_SELECTED_TEXT:${1:alt}}](https://${2:link})$0", "description": "Insert image" + }, + "Insert strikethrough": { + "prefix": "strikethrough", + "body": "~~${1:${TM_SELECTED_TEXT}}~~", + "description": "Insert strikethrough" } } diff --git a/extensions/markdown-language-features/src/test/inMemoryDocument.ts b/extensions/markdown-language-features/src/test/inMemoryDocument.ts index c2472e5a4ec..052216f90f5 100644 --- a/extensions/markdown-language-features/src/test/inMemoryDocument.ts +++ b/extensions/markdown-language-features/src/test/inMemoryDocument.ts @@ -22,6 +22,7 @@ export class InMemoryDocument implements vscode.TextDocument { isDirty: boolean = false; isClosed: boolean = false; eol: vscode.EndOfLine = vscode.EndOfLine.LF; + notebook: undefined; get fileName(): string { return this.uri.fsPath; @@ -66,4 +67,4 @@ export class InMemoryDocument implements vscode.TextDocument { save(): never { throw new Error('Method not implemented.'); } -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/src/test/index.ts b/extensions/markdown-language-features/src/test/index.ts index 77019228745..0eb9bc92487 100644 --- a/extensions/markdown-language-features/src/test/index.ts +++ b/extensions/markdown-language-features/src/test/index.ts @@ -6,21 +6,31 @@ const path = require('path'); const testRunner = require('vscode/lib/testrunner'); -const suite = 'Integration Markdown Tests'; - const options: any = { ui: 'tdd', useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), timeout: 60000 }; +// These integration tests is being run in multiple environments (electron, web, remote) +// so we need to set the suite name based on the environment as the suite name is used +// for the test results file name +let suite = ''; +if (process.env.VSCODE_BROWSER) { + suite = `${process.env.VSCODE_BROWSER} Browser Integration Markdown Tests`; +} else if (process.env.REMOTE_VSCODE) { + suite = 'Remote Integration Markdown Tests'; +} else { + suite = 'Integration Markdown Tests'; +} + if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 81ed5c32e4f..64becb50468 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -12,7 +12,6 @@ ], "enableProposedApi": true, "activationEvents": [ - "*", "onAuthenticationRequest:microsoft" ], "extensionKind": [ @@ -20,6 +19,14 @@ "workspace", "web" ], + "contributes": { + "authentication": [ + { + "label": "Microsoft", + "id": "microsoft" + } + ] + }, "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", "main": "./out/extension.js", "browser": "./dist/browser/extension.js", diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index 035c5af7350..67b55889c3c 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -5,6 +5,7 @@ import * as randomBytes from 'randombytes'; import * as querystring from 'querystring'; +import { Buffer } from 'buffer'; import * as vscode from 'vscode'; import { createServer, startServer } from './authServer'; diff --git a/extensions/npm/package.json b/extensions/npm/package.json index a77b48647cd..aa478b26c9b 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -56,7 +56,6 @@ { "id": "npm", "name": "%view.name%", - "when": "npm:showScriptExplorer", "icon": "images/code.svg", "visibility": "hidden" } @@ -166,14 +165,9 @@ "when": "view == npm && viewItem == script", "group": "inline" }, - { - "command": "npm.runScript", - "when": "view == npm && viewItem == debugScript", - "group": "inline" - }, { "command": "npm.debugScript", - "when": "view == npm && viewItem == debugScript", + "when": "view == npm && viewItem == script", "group": "inline" }, { @@ -237,6 +231,7 @@ "type": "boolean", "default": false, "scope": "resource", + "deprecationMessage": "The NPM Script Explorer is now available in 'Views' menu in the Explorer in all folders.", "description": "%config.npm.enableScriptExplorer%" }, "npm.enableRunFromFolder": { diff --git a/extensions/npm/src/features/bowerJSONContribution.ts b/extensions/npm/src/features/bowerJSONContribution.ts index cd648732fc7..c3a827fd1e1 100644 --- a/extensions/npm/src/features/bowerJSONContribution.ts +++ b/extensions/npm/src/features/bowerJSONContribution.ts @@ -33,7 +33,7 @@ export class BowerJSONContribution implements IJSONContribution { return [{ language: 'json', scheme: '*', pattern: '**/bower.json' }, { language: 'json', scheme: '*', pattern: '**/.bower.json' }]; } - private onlineEnabled() { + private isEnabled() { return !!workspace.getConfiguration('npm').get('fetchOnlinePackageInfo'); } @@ -54,8 +54,11 @@ export class BowerJSONContribution implements IJSONContribution { } public collectPropertySuggestions(_resource: string, location: Location, currentWord: string, addValue: boolean, isLast: boolean, collector: ISuggestionsCollector): Thenable | null { + if (!this.isEnabled()) { + return null; + } if ((location.matches(['dependencies']) || location.matches(['devDependencies']))) { - if (currentWord.length > 0 && this.onlineEnabled()) { + if (currentWord.length > 0) { const queryUrl = 'https://registry.bower.io/packages/search/' + encodeURIComponent(currentWord); return this.xhr({ @@ -122,7 +125,10 @@ export class BowerJSONContribution implements IJSONContribution { return null; } - public collectValueSuggestions(_resource: string, location: Location, collector: ISuggestionsCollector): Thenable { + public collectValueSuggestions(_resource: string, location: Location, collector: ISuggestionsCollector): Promise | null { + if (!this.isEnabled()) { + return null; + } if ((location.matches(['dependencies', '*']) || location.matches(['devDependencies', '*']))) { // not implemented. Could be do done calling the bower command. Waiting for web API: https://github.com/bower/registry/issues/26 const proposal = new CompletionItem(localize('json.bower.latest.version', 'latest')); @@ -132,7 +138,7 @@ export class BowerJSONContribution implements IJSONContribution { proposal.documentation = 'The latest version of the package'; collector.add(proposal); } - return Promise.resolve(null); + return null; } public resolveSuggestion(item: CompletionItem): Thenable | null { @@ -149,10 +155,6 @@ export class BowerJSONContribution implements IJSONContribution { } private getInfo(pack: string): Thenable { - if (!this.onlineEnabled()) { - return Promise.resolve(undefined); - } - const queryUrl = 'https://registry.bower.io/packages/' + encodeURIComponent(pack); return this.xhr({ @@ -181,6 +183,9 @@ export class BowerJSONContribution implements IJSONContribution { } public getInfoContribution(_resource: string, location: Location): Thenable | null { + if (!this.isEnabled()) { + return null; + } if ((location.matches(['dependencies', '*']) || location.matches(['devDependencies', '*']))) { const pack = location.path[location.path.length - 1]; if (typeof pack === 'string') { diff --git a/extensions/npm/src/features/jsonContributions.ts b/extensions/npm/src/features/jsonContributions.ts index 3873b2dc31a..071d57b3348 100644 --- a/extensions/npm/src/features/jsonContributions.ts +++ b/extensions/npm/src/features/jsonContributions.ts @@ -25,7 +25,7 @@ export interface IJSONContribution { getDocumentSelector(): DocumentSelector; getInfoContribution(fileName: string, location: Location): Thenable | null; collectPropertySuggestions(fileName: string, location: Location, currentWord: string, addValue: boolean, isLast: boolean, result: ISuggestionsCollector): Thenable | null; - collectValueSuggestions(fileName: string, location: Location, result: ISuggestionsCollector): Thenable; + collectValueSuggestions(fileName: string, location: Location, result: ISuggestionsCollector): Thenable | null; collectDefaultSuggestions(fileName: string, result: ISuggestionsCollector): Thenable; resolveSuggestion?(item: CompletionItem): Thenable | null; } diff --git a/extensions/npm/src/features/packageJSONContribution.ts b/extensions/npm/src/features/packageJSONContribution.ts index 135b632071c..f154a875239 100644 --- a/extensions/npm/src/features/packageJSONContribution.ts +++ b/extensions/npm/src/features/packageJSONContribution.ts @@ -51,6 +51,10 @@ export class PackageJSONContribution implements IJSONContribution { return Promise.resolve(null); } + private isEnabled() { + return this.canRunNPM || this.onlineEnabled(); + } + private onlineEnabled() { return !!workspace.getConfiguration('npm').get('fetchOnlinePackageInfo'); } @@ -63,7 +67,7 @@ export class PackageJSONContribution implements IJSONContribution { isLast: boolean, collector: ISuggestionsCollector ): Thenable | null { - if (!this.onlineEnabled()) { + if (!this.isEnabled()) { return null; } @@ -180,7 +184,7 @@ export class PackageJSONContribution implements IJSONContribution { } public async collectValueSuggestions(_fileName: string, location: Location, result: ISuggestionsCollector): Promise { - if (!this.onlineEnabled()) { + if (!this.isEnabled()) { return null; } @@ -245,18 +249,37 @@ export class PackageJSONContribution implements IJSONContribution { return null; } + private isValidNPMName(name: string): boolean { + // following rules from https://github.com/npm/validate-npm-package-name + if (!name || name.length > 214 || name.match(/^[_.]/)) { + return false; + } + const match = name.match(/^(?:@([^/]+?)[/])?([^/]+?)$/); + if (match) { + const scope = match[1]; + if (scope && encodeURIComponent(scope) !== scope) { + return false; + } + const name = match[2]; + return encodeURIComponent(name) === name; + } + return true; + } + private async fetchPackageInfo(pack: string): Promise { + if (!this.isValidNPMName(pack)) { + return undefined; // avoid unnecessary lookups + } let info: ViewPackageInfo | undefined; if (this.canRunNPM) { info = await this.npmView(pack); } - if (!info) { + if (!info && this.onlineEnabled()) { info = await this.npmjsView(pack); } return info; } - private npmView(pack: string): Promise { return new Promise((resolve, _reject) => { const command = 'npm view --json ' + pack + ' description dist-tags.latest homepage version'; @@ -303,6 +326,9 @@ export class PackageJSONContribution implements IJSONContribution { } public getInfoContribution(_fileName: string, location: Location): Thenable | null { + if (!this.isEnabled()) { + return null; + } if ((location.matches(['dependencies', '*']) || location.matches(['devDependencies', '*']) || location.matches(['optionalDependencies', '*']) || location.matches(['peerDependencies', '*']))) { const pack = location.path[location.path.length - 1]; if (typeof pack === 'string') { diff --git a/extensions/npm/src/npmMain.ts b/extensions/npm/src/npmMain.ts index 764be6ea0fc..a92f554b759 100644 --- a/extensions/npm/src/npmMain.ts +++ b/extensions/npm/src/npmMain.ts @@ -8,7 +8,7 @@ import * as vscode from 'vscode'; import { addJSONProviders } from './features/jsonContributions'; import { runSelectedScript, selectAndRunScriptFromFolder } from './commands'; import { NpmScriptsTreeDataProvider } from './npmView'; -import { invalidateTasksCache, NpmTaskProvider, hasPackageJson } from './tasks'; +import { invalidateTasksCache, NpmTaskProvider } from './tasks'; import { invalidateHoverScriptsCache, NpmScriptHoverProvider } from './scriptHover'; let treeDataProvider: NpmScriptsTreeDataProvider | undefined; @@ -44,11 +44,6 @@ export async function activate(context: vscode.ExtensionContext): Promise registerHoverProvider(context); context.subscriptions.push(vscode.commands.registerCommand('npm.runSelectedScript', runSelectedScript)); - - if (await hasPackageJson()) { - vscode.commands.executeCommand('setContext', 'npm:showScriptExplorer', true); - } - context.subscriptions.push(vscode.commands.registerCommand('npm.runScriptFromFolder', selectAndRunScriptFromFolder)); } diff --git a/extensions/npm/src/npmView.ts b/extensions/npm/src/npmView.ts index 0a4908ec440..c7c7835fa04 100644 --- a/extensions/npm/src/npmView.ts +++ b/extensions/npm/src/npmView.ts @@ -3,18 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { JSONVisitor, visit } from 'jsonc-parser'; import * as path from 'path'; import { - Event, EventEmitter, ExtensionContext, Task2 as Task, - TextDocument, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, Uri, - WorkspaceFolder, commands, window, workspace, tasks, Selection, TaskGroup + commands, Event, EventEmitter, ExtensionContext, + Selection, Task, + TaskGroup, tasks, TextDocument, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, Uri, + window, workspace, WorkspaceFolder } from 'vscode'; -import { visit, JSONVisitor } from 'jsonc-parser'; -import { - NpmTaskDefinition, getPackageJsonUriFromTask, getScripts, - isWorkspaceFolder, getTaskName, createTask, extractDebugArgFromScript, startDebugging, isAutoDetectionEnabled -} from './tasks'; import * as nls from 'vscode-nls'; +import { + createTask, getTaskName, isAutoDetectionEnabled, isWorkspaceFolder, NpmTaskDefinition, + startDebugging +} from './tasks'; const localize = nls.loadMessageBundle(); @@ -90,9 +91,6 @@ class NpmScript extends TreeItem { } }; this.contextValue = 'script'; - if (task.group && task.group === TaskGroup.Rebuild) { - this.contextValue = 'debugScript'; - } this.package = packageJson; this.task = task; this.command = commandList[command]; @@ -139,27 +137,8 @@ export class NpmScriptsTreeDataProvider implements TreeDataProvider { tasks.executeTask(script.task); } - private extractDebugArg(scripts: any, task: Task): [string, number] | undefined { - return extractDebugArgFromScript(scripts[task.name]); - } - private async debugScript(script: NpmScript) { - let task = script.task; - let uri = getPackageJsonUriFromTask(task); - let scripts = await getScripts(uri!); - - let debugArg = this.extractDebugArg(scripts, task); - if (!debugArg) { - let message = localize('noDebugOptions', 'Could not launch "{0}" for debugging because the scripts lacks a node debug option, e.g. "--inspect-brk".', task.name); - let learnMore = localize('learnMore', 'Learn More'); - let ok = localize('ok', 'OK'); - let result = await window.showErrorMessage(message, { modal: true }, ok, learnMore); - if (result === learnMore) { - commands.executeCommand('vscode.open', Uri.parse('https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_launch-configuration-support-for-npm-and-other-tools')); - } - return; - } - startDebugging(task.name, debugArg[0], debugArg[1], script.getFolder()); + startDebugging(script.task.name, script.getFolder()); } private findScript(document: TextDocument, script?: NpmScript): number { diff --git a/extensions/npm/src/scriptHover.ts b/extensions/npm/src/scriptHover.ts index aa803dbc1d4..f8a5482bef8 100644 --- a/extensions/npm/src/scriptHover.ts +++ b/extensions/npm/src/scriptHover.ts @@ -8,7 +8,7 @@ import { workspace, tasks, Range, HoverProvider, Hover, Position, MarkdownString, Uri } from 'vscode'; import { - createTask, startDebugging, findAllScriptRanges, extractDebugArgFromScript + createTask, startDebugging, findAllScriptRanges } from './tasks'; import * as nls from 'vscode-nls'; @@ -54,11 +54,7 @@ export class NpmScriptHoverProvider implements HoverProvider { let contents: MarkdownString = new MarkdownString(); contents.isTrusted = true; contents.appendMarkdown(this.createRunScriptMarkdown(key, document.uri)); - - let debugArgs = extractDebugArgFromScript(value[2]); - if (debugArgs) { - contents.appendMarkdown(this.createDebugScriptMarkdown(key, document.uri, debugArgs[0], debugArgs[1])); - } + contents.appendMarkdown(this.createDebugScriptMarkdown(key, document.uri)); hover = new Hover(contents); } }); @@ -78,12 +74,10 @@ export class NpmScriptHoverProvider implements HoverProvider { ); } - private createDebugScriptMarkdown(script: string, documentUri: Uri, protocol: string, port: number): string { - let args = { + private createDebugScriptMarkdown(script: string, documentUri: Uri): string { + const args = { documentUri: documentUri, script: script, - protocol: protocol, - port: port }; return this.createMarkdownLink( localize('debugScript', 'Debug Script'), @@ -116,11 +110,9 @@ export class NpmScriptHoverProvider implements HoverProvider { public debugScriptFromHover(args: any) { let script = args.script; let documentUri = args.documentUri; - let protocol = args.protocol; - let port = args.port; let folder = workspace.getWorkspaceFolder(documentUri); if (folder) { - startDebugging(script, protocol, port, folder); + startDebugging(script, folder); } } } diff --git a/extensions/npm/src/tasks.ts b/extensions/npm/src/tasks.ts index 2f4947de447..f7def2f8876 100644 --- a/extensions/npm/src/tasks.ts +++ b/extensions/npm/src/tasks.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { - TaskDefinition, Task2 as Task, TaskGroup, WorkspaceFolder, RelativePattern, ShellExecution, Uri, workspace, + TaskDefinition, Task, TaskGroup, WorkspaceFolder, RelativePattern, ShellExecution, Uri, workspace, DebugConfiguration, debug, TaskProvider, TextDocument, tasks, TaskScope, QuickPickItem } from 'vscode'; import * as path from 'path'; @@ -249,6 +249,8 @@ async function provideNpmScriptsForFolder(packageJsonUri: Uri): Promise if (prePostScripts.has(each)) { task.group = TaskGroup.Clean; // hack: use Clean group to tag pre/post scripts } + + // todo@connor4312: all scripts are now debuggable, what is a 'debug script'? if (isDebugScript(scripts![each])) { task.group = TaskGroup.Rebuild; // hack: use Rebuild group to tag debug scripts } @@ -355,44 +357,16 @@ export function runScript(script: string, document: TextDocument) { } } -export function extractDebugArgFromScript(scriptValue: string): [string, number] | undefined { - // matches --debug, --debug=1234, --debug-brk, debug-brk=1234, --inspect, - // --inspect=1234, --inspect-brk, --inspect-brk=1234, - // --inspect=localhost:1245, --inspect=127.0.0.1:1234, --inspect=[aa:1:0:0:0]:1234, --inspect=:1234 - let match = scriptValue.match(/--(inspect|debug)(-brk)?(=((\[[0-9a-fA-F:]*\]|[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+|[a-zA-Z0-9\.]*):)?(\d+))?/); - - if (match) { - if (match[6]) { - return [match[1], parseInt(match[6])]; - } - if (match[1] === 'inspect') { - return [match[1], 9229]; - } - if (match[1] === 'debug') { - return [match[1], 5858]; - } - } - return undefined; -} - -export function startDebugging(scriptName: string, protocol: string, port: number, folder: WorkspaceFolder) { - let p = 'inspector'; - if (protocol === 'debug') { - p = 'legacy'; - } - - let packageManager = getPackageManager(folder); +export function startDebugging(scriptName: string, folder: WorkspaceFolder) { const config: DebugConfiguration = { - type: 'node', + type: 'pwa-node', request: 'launch', name: `Debug ${scriptName}`, - runtimeExecutable: packageManager, + runtimeExecutable: getPackageManager(folder), runtimeArgs: [ 'run', scriptName, ], - port: port, - protocol: p }; if (folder) { diff --git a/extensions/package.json b/extensions/package.json index 665553eeeae..97870c8d51b 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "3.9.7" + "typescript": "4.0.2" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/php/cgmanifest.json b/extensions/php/cgmanifest.json index f265c4b3184..ea157efc34e 100644 --- a/extensions/php/cgmanifest.json +++ b/extensions/php/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "language-php", "repositoryUrl": "https://github.com/atom/language-php", - "commitHash": "882f6c0e19f0ebf9dafa443bf4c3fc5626f76aed" + "commitHash": "11cdaf62a9d949d3aca550f1a58c9754de6b5ab0" } }, "license": "MIT", - "version": "0.44.4" + "version": "0.44.5" } ], "version": 1 diff --git a/extensions/php/syntaxes/php.tmLanguage.json b/extensions/php/syntaxes/php.tmLanguage.json index f04272f72f5..a995a1e82ab 100644 --- a/extensions/php/syntaxes/php.tmLanguage.json +++ b/extensions/php/syntaxes/php.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/atom/language-php/commit/882f6c0e19f0ebf9dafa443bf4c3fc5626f76aed", + "version": "https://github.com/atom/language-php/commit/11cdaf62a9d949d3aca550f1a58c9754de6b5ab0", "scopeName": "source.php", "patterns": [ { @@ -146,7 +146,7 @@ "name": "keyword.other.use.php" } }, - "end": "(?<=})|(?=;)", + "end": "(?<=})|(?=;)|(?=\\?>)", "name": "meta.use.php", "patterns": [ { diff --git a/extensions/python/cgmanifest.json b/extensions/python/cgmanifest.json index 6b1df10ed8f..37a21b2de54 100644 --- a/extensions/python/cgmanifest.json +++ b/extensions/python/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "MagicStack/MagicPython", "repositoryUrl": "https://github.com/MagicStack/MagicPython", - "commitHash": "b4b2e6eb16fee36aea0788bf0aa1853c25f7d276" + "commitHash": "c9b3409deb69acec31bbf7913830e93a046b30cc" } }, "license": "MIT", diff --git a/extensions/python/syntaxes/MagicPython.tmLanguage.json b/extensions/python/syntaxes/MagicPython.tmLanguage.json index b8822299e63..0df9076dfc9 100644 --- a/extensions/python/syntaxes/MagicPython.tmLanguage.json +++ b/extensions/python/syntaxes/MagicPython.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/MagicStack/MagicPython/commit/b4b2e6eb16fee36aea0788bf0aa1853c25f7d276", + "version": "https://github.com/MagicStack/MagicPython/commit/b2b4f4ae7b4e6284e80bda8080106b93bd588f9e", "name": "MagicPython", "scopeName": "source.python", "patterns": [ @@ -634,9 +634,6 @@ }, "2": { "name": "invalid.illegal.dec.python" - }, - "3": { - "name": "invalid.illegal.dec.python" } } }, diff --git a/extensions/search-result/syntaxes/generateTMLanguage.js b/extensions/search-result/syntaxes/generateTMLanguage.js index eac084ddbc1..fb74d3696ef 100644 --- a/extensions/search-result/syntaxes/generateTMLanguage.js +++ b/extensions/search-result/syntaxes/generateTMLanguage.js @@ -3,10 +3,9 @@ const mappings = [ ['bat', 'source.batchfile'], ['c', 'source.c'], - ['cc', 'source.cpp'], ['clj', 'source.clojure'], ['coffee', 'source.coffee'], - ['cpp', 'source.cpp'], + ['cpp', 'source.cpp', '\\.(?:cpp|c\\+\\+|cc|cxx|hxx|h\\+\\+|hh)'], ['cs', 'source.cs'], ['cshtml', 'text.html.cshtml'], ['css', 'source.css'], @@ -17,8 +16,7 @@ const mappings = [ ['go', 'source.go'], ['groovy', 'source.groovy'], ['h', 'source.objc'], - ['handlebars', 'text.html.handlebars'], - ['hbs', 'text.html.handlebars'], + ['handlebars', 'text.html.handlebars', '\\.(?:handlebars|hbs)'], ['hlsl', 'source.hlsl'], ['hpp', 'source.objcpp'], ['html', 'text.html.basic'], @@ -35,10 +33,8 @@ const mappings = [ ['md', 'text.html.markdown'], ['mm', 'source.objcpp'], ['p6', 'source.perl.6'], - ['perl', 'source.perl'], + ['perl', 'source.perl', '\\.(?:perl|pl|pm)'], ['php', 'source.php'], - ['pl', 'source.perl'], - ['pm', 'source.perl'], ['ps1', 'source.powershell'], ['pug', 'text.pug'], ['py', 'source.python'], @@ -54,8 +50,7 @@ const mappings = [ ['tsx', 'source.tsx'], ['vb', 'source.asp.vb.net'], ['xml', 'text.xml'], - ['yaml', 'source.yaml'], - ['yml', 'source.yaml'], + ['yaml', 'source.yaml', '\\.(?:ya?ml)'], ]; const scopes = { diff --git a/extensions/search-result/syntaxes/searchResult.tmLanguage.json b/extensions/search-result/syntaxes/searchResult.tmLanguage.json index a8a5557c3e5..e2687fe8a72 100644 --- a/extensions/search-result/syntaxes/searchResult.tmLanguage.json +++ b/extensions/search-result/syntaxes/searchResult.tmLanguage.json @@ -84,9 +84,6 @@ { "include": "#c" }, - { - "include": "#cc" - }, { "include": "#clj" }, @@ -129,9 +126,6 @@ { "include": "#handlebars" }, - { - "include": "#hbs" - }, { "include": "#hlsl" }, @@ -186,12 +180,6 @@ { "include": "#php" }, - { - "include": "#pl" - }, - { - "include": "#pm" - }, { "include": "#ps1" }, @@ -240,9 +228,6 @@ { "include": "#yaml" }, - { - "include": "#yml" - }, { "match": "^(?!\\s)(.*?)([^\\\\\\/\\n]*)(:)$", "name": "meta.resultBlock.search string meta.path.search", @@ -453,92 +438,6 @@ } ] }, - "cc": { - "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.cc)(:)$", - "end": "^(?!\\s)", - "beginCaptures": { - "0": { - "name": "string meta.path.search" - }, - "1": { - "name": "meta.path.dirname.search" - }, - "2": { - "name": "meta.path.basename.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "name": "meta.resultLine.search meta.resultLine.multiLine.search", - "begin": "^ (?:\\s*)((\\d+) )", - "while": "^ (?:\\s*)(?:((\\d+)(:))|((\\d+) ))", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "whileCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - }, - "4": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "5": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "patterns": [ - { - "include": "source.cpp" - } - ] - }, - { - "begin": "^ (?:\\s*)((\\d+)(:))", - "while": "(?=not)possible", - "name": "meta.resultLine.search meta.resultLine.singleLine.search", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "include": "source.cpp" - } - ] - } - ] - }, "clj": { "name": "meta.resultBlock.search", "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.clj)(:)$", @@ -713,7 +612,7 @@ }, "cpp": { "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.cpp)(:)$", + "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.(?:cpp|c\\+\\+|cc|cxx|hxx|h\\+\\+|hh))(:)$", "end": "^(?!\\s)", "beginCaptures": { "0": { @@ -1229,7 +1128,7 @@ }, "dockerfile": { "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*(?:dockerfile|Dockerfile))(:)$", + "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*(?:dockerfile|Dockerfile|containerfile|Containerfile))(:)$", "end": "^(?!\\s)", "beginCaptures": { "0": { @@ -1659,93 +1558,7 @@ }, "handlebars": { "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.handlebars)(:)$", - "end": "^(?!\\s)", - "beginCaptures": { - "0": { - "name": "string meta.path.search" - }, - "1": { - "name": "meta.path.dirname.search" - }, - "2": { - "name": "meta.path.basename.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "name": "meta.resultLine.search meta.resultLine.multiLine.search", - "begin": "^ (?:\\s*)((\\d+) )", - "while": "^ (?:\\s*)(?:((\\d+)(:))|((\\d+) ))", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "whileCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - }, - "4": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "5": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "patterns": [ - { - "include": "text.html.handlebars" - } - ] - }, - { - "begin": "^ (?:\\s*)((\\d+)(:))", - "while": "(?=not)possible", - "name": "meta.resultLine.search meta.resultLine.singleLine.search", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "include": "text.html.handlebars" - } - ] - } - ] - }, - "hbs": { - "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.hbs)(:)$", + "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.(?:handlebars|hbs))(:)$", "end": "^(?!\\s)", "beginCaptures": { "0": { @@ -3207,7 +3020,7 @@ }, "perl": { "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.perl)(:)$", + "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.(?:perl|pl|pm))(:)$", "end": "^(?!\\s)", "beginCaptures": { "0": { @@ -3377,178 +3190,6 @@ } ] }, - "pl": { - "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.pl)(:)$", - "end": "^(?!\\s)", - "beginCaptures": { - "0": { - "name": "string meta.path.search" - }, - "1": { - "name": "meta.path.dirname.search" - }, - "2": { - "name": "meta.path.basename.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "name": "meta.resultLine.search meta.resultLine.multiLine.search", - "begin": "^ (?:\\s*)((\\d+) )", - "while": "^ (?:\\s*)(?:((\\d+)(:))|((\\d+) ))", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "whileCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - }, - "4": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "5": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "patterns": [ - { - "include": "source.perl" - } - ] - }, - { - "begin": "^ (?:\\s*)((\\d+)(:))", - "while": "(?=not)possible", - "name": "meta.resultLine.search meta.resultLine.singleLine.search", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "include": "source.perl" - } - ] - } - ] - }, - "pm": { - "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.pm)(:)$", - "end": "^(?!\\s)", - "beginCaptures": { - "0": { - "name": "string meta.path.search" - }, - "1": { - "name": "meta.path.dirname.search" - }, - "2": { - "name": "meta.path.basename.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "name": "meta.resultLine.search meta.resultLine.multiLine.search", - "begin": "^ (?:\\s*)((\\d+) )", - "while": "^ (?:\\s*)(?:((\\d+)(:))|((\\d+) ))", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "whileCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - }, - "4": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "5": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "patterns": [ - { - "include": "source.perl" - } - ] - }, - { - "begin": "^ (?:\\s*)((\\d+)(:))", - "while": "(?=not)possible", - "name": "meta.resultLine.search meta.resultLine.singleLine.search", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "include": "source.perl" - } - ] - } - ] - }, "ps1": { "name": "meta.resultBlock.search", "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.ps1)(:)$", @@ -4841,93 +4482,7 @@ }, "yaml": { "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.yaml)(:)$", - "end": "^(?!\\s)", - "beginCaptures": { - "0": { - "name": "string meta.path.search" - }, - "1": { - "name": "meta.path.dirname.search" - }, - "2": { - "name": "meta.path.basename.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "name": "meta.resultLine.search meta.resultLine.multiLine.search", - "begin": "^ (?:\\s*)((\\d+) )", - "while": "^ (?:\\s*)(?:((\\d+)(:))|((\\d+) ))", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "whileCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - }, - "4": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "5": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "patterns": [ - { - "include": "source.yaml" - } - ] - }, - { - "begin": "^ (?:\\s*)((\\d+)(:))", - "while": "(?=not)possible", - "name": "meta.resultLine.search meta.resultLine.singleLine.search", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "include": "source.yaml" - } - ] - } - ] - }, - "yml": { - "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.yml)(:)$", + "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.(?:ya?ml))(:)$", "end": "^(?!\\s)", "beginCaptures": { "0": { diff --git a/extensions/swift/syntaxes/swift.tmLanguage.json b/extensions/swift/syntaxes/swift.tmLanguage.json index 33cb2ca044a..91a374d4716 100644 --- a/extensions/swift/syntaxes/swift.tmLanguage.json +++ b/extensions/swift/syntaxes/swift.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/textmate/swift.tmbundle/commit/ecba759c1c2f46f69795fe2d01691030214dd5ff", + "version": "https://github.com/textmate/swift.tmbundle/commit/97d29d2073853c328e42239c5d38c96e2e2ade9c", "name": "Swift", "scopeName": "source.swift", "comment": "See swift.tmbundle/grammar-test.swift for test cases.", @@ -2620,7 +2620,7 @@ "name": "variable.language.swift" }, { - "match": "\\B(?:#file|#filePath|#line|#column|#function|#dsohandle)\\b|\\b(?:__FILE__|__LINE__|__COLUMN__|__FUNCTION__|__DSO_HANDLE__)\\b", + "match": "\\B(?:#file|#filePath|#fileID|#line|#column|#function|#dsohandle)\\b|\\b(?:__FILE__|__LINE__|__COLUMN__|__FUNCTION__|__DSO_HANDLE__)\\b", "name": "support.variable.swift" }, { diff --git a/extensions/theme-abyss/themes/abyss-color-theme.json b/extensions/theme-abyss/themes/abyss-color-theme.json index 39f93305b8d..7afe3bd963e 100644 --- a/extensions/theme-abyss/themes/abyss-color-theme.json +++ b/extensions/theme-abyss/themes/abyss-color-theme.json @@ -233,6 +233,20 @@ "foreground": "#22aa44" } }, + { + "name": "Markup: Strong", + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Emphasis", + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, { "name": "Markup Inline", "scope": "markup.inline.raw", @@ -242,11 +256,14 @@ } }, { - "name": "Markup Setext Header", - "scope": "markup.heading.setext", + "name": "Markup Headings", + "scope": [ + "markup.heading", + "markup.heading.setext" + ], "settings": { - "fontStyle": "", - "foreground": "#ddbb88" + "fontStyle": "bold", + "foreground": "#6688cc" } } ], diff --git a/extensions/theme-defaults/themes/hc_black_defaults.json b/extensions/theme-defaults/themes/hc_black_defaults.json index 1a03010abff..495a15238dc 100644 --- a/extensions/theme-defaults/themes/hc_black_defaults.json +++ b/extensions/theme-defaults/themes/hc_black_defaults.json @@ -136,6 +136,7 @@ { "scope": "markup.heading", "settings": { + "fontStyle": "bold", "foreground": "#6796e6" } }, diff --git a/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json b/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json index cdd22307117..38c8fe09968 100644 --- a/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json +++ b/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json @@ -260,7 +260,7 @@ "entity.name.section" ], "settings": { - "fontStyle": "", + "fontStyle": "bold", "foreground": "#8ab1b0" } }, diff --git a/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json b/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json index 935573463ee..5140f5ad3d0 100644 --- a/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json +++ b/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json @@ -557,6 +557,65 @@ "foreground": "#D0B344" } }, + { + "name": "Markdown Headings", + "scope": "markup.heading.markdown", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markdown Quote", + "scope": "markup.quote.markdown", + "settings": { + "fontStyle": "italic", + "foreground": "" + } + }, + { + "name": "Markdown Bold", + "scope": "markup.bold.markdown", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markdown Link Title/Description", + "scope": "string.other.link.title.markdown,string.other.link.description.markdown", + "settings": { + "foreground": "#AE81FF" + } + }, + { + "name": "Markdown Underline Link/Image", + "scope": "markup.underline.link.markdown,markup.underline.link.image.markdown", + "settings": { + "foreground": "" + } + }, + { + "name": "Markdown Emphasis", + "scope": "markup.italic.markdown", + "settings": { + "fontStyle": "italic" + } + }, + { + "name": "Markdown Punctuation Definition Link", + "scope": "markup.list.unnumbered.markdown, markup.list.numbered.markdown", + "settings": { + "foreground": "" + } + }, + { + "name": "Markdown List Punctuation", + "scope": [ + "punctuation.definition.list.begin.markdown" + ], + "settings": { + "foreground": "" + } + }, { "scope": "token.info-token", "settings": { diff --git a/extensions/theme-red/themes/Red-color-theme.json b/extensions/theme-red/themes/Red-color-theme.json index 8964f40a093..dbe80113209 100644 --- a/extensions/theme-red/themes/Red-color-theme.json +++ b/extensions/theme-red/themes/Red-color-theme.json @@ -350,6 +350,20 @@ "foreground": "#fb9a4bff" } }, + { + "name": "Markup: Strong", + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Emphasis", + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, { "name": "Markup Inline", "scope": "markup.inline.raw", @@ -359,17 +373,15 @@ } }, { - "name": "Markup Headings", - "scope": "markup.heading", + "name": "Headings", + "scope": [ + "markup.heading", + "markup.heading.setext", + "punctuation.definition.heading", + "entity.name.section" + ], "settings": { - "foreground": "#fec758ff" - } - }, - { - "name": "Markup Setext Header", - "scope": "markup.heading.setext", - "settings": { - "fontStyle": "", + "fontStyle": "bold", "foreground": "#fec758ff" } }, diff --git a/extensions/theme-seti/cgmanifest.json b/extensions/theme-seti/cgmanifest.json index b3bb41d3eb9..8899dae032b 100644 --- a/extensions/theme-seti/cgmanifest.json +++ b/extensions/theme-seti/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "seti-ui", "repositoryUrl": "https://github.com/jesseweed/seti-ui", - "commitHash": "f3b2775662b0075aab56e5f0c03269f21f3f0f30" + "commitHash": "719e5d384e878b0e190abc80247a8726f083a393" } }, "version": "0.1.0" diff --git a/extensions/theme-seti/icons/seti.woff b/extensions/theme-seti/icons/seti.woff index 5dc3bb8d9b0..e1a5a634497 100644 Binary files a/extensions/theme-seti/icons/seti.woff and b/extensions/theme-seti/icons/seti.woff differ diff --git a/extensions/theme-seti/icons/vs-seti-icon-theme.json b/extensions/theme-seti/icons/vs-seti-icon-theme.json index a1f5817da5c..29712ae6b91 100644 --- a/extensions/theme-seti/icons/vs-seti-icon-theme.json +++ b/extensions/theme-seti/icons/vs-seti-icon-theme.json @@ -254,1119 +254,1143 @@ "fontCharacter": "\\E01C", "fontColor": "#8dc149" }, - "_d_light": { + "_cu_light": { "fontCharacter": "\\E01D", + "fontColor": "#7fae42" + }, + "_cu": { + "fontCharacter": "\\E01D", + "fontColor": "#8dc149" + }, + "_cu_1_light": { + "fontCharacter": "\\E01D", + "fontColor": "#9068b0" + }, + "_cu_1": { + "fontCharacter": "\\E01D", + "fontColor": "#a074c4" + }, + "_d_light": { + "fontCharacter": "\\E01E", "fontColor": "#b8383d" }, "_d": { - "fontCharacter": "\\E01D", + "fontCharacter": "\\E01E", "fontColor": "#cc3e44" }, "_dart_light": { - "fontCharacter": "\\E01E", + "fontCharacter": "\\E01F", "fontColor": "#498ba7" }, "_dart": { - "fontCharacter": "\\E01E", + "fontCharacter": "\\E01F", "fontColor": "#519aba" }, "_db_light": { - "fontCharacter": "\\E01F", + "fontCharacter": "\\E020", "fontColor": "#dd4b78" }, "_db": { - "fontCharacter": "\\E01F", + "fontCharacter": "\\E020", "fontColor": "#f55385" }, "_default_light": { - "fontCharacter": "\\E020", + "fontCharacter": "\\E021", "fontColor": "#bfc2c1" }, "_default": { - "fontCharacter": "\\E020", + "fontCharacter": "\\E021", "fontColor": "#d4d7d6" }, "_docker_light": { - "fontCharacter": "\\E022", + "fontCharacter": "\\E023", "fontColor": "#498ba7" }, "_docker": { - "fontCharacter": "\\E022", + "fontCharacter": "\\E023", "fontColor": "#519aba" }, "_docker_1_light": { - "fontCharacter": "\\E022", + "fontCharacter": "\\E023", "fontColor": "#455155" }, "_docker_1": { - "fontCharacter": "\\E022", + "fontCharacter": "\\E023", "fontColor": "#4d5a5e" }, "_docker_2_light": { - "fontCharacter": "\\E022", + "fontCharacter": "\\E023", "fontColor": "#7fae42" }, "_docker_2": { - "fontCharacter": "\\E022", + "fontCharacter": "\\E023", "fontColor": "#8dc149" }, "_docker_3_light": { - "fontCharacter": "\\E022", + "fontCharacter": "\\E023", "fontColor": "#dd4b78" }, "_docker_3": { - "fontCharacter": "\\E022", + "fontCharacter": "\\E023", "fontColor": "#f55385" }, "_ejs_light": { - "fontCharacter": "\\E024", + "fontCharacter": "\\E025", "fontColor": "#b7b73b" }, "_ejs": { - "fontCharacter": "\\E024", + "fontCharacter": "\\E025", "fontColor": "#cbcb41" }, "_elixir_light": { - "fontCharacter": "\\E025", + "fontCharacter": "\\E026", "fontColor": "#9068b0" }, "_elixir": { - "fontCharacter": "\\E025", + "fontCharacter": "\\E026", "fontColor": "#a074c4" }, "_elixir_script_light": { - "fontCharacter": "\\E026", + "fontCharacter": "\\E027", "fontColor": "#9068b0" }, "_elixir_script": { - "fontCharacter": "\\E026", + "fontCharacter": "\\E027", "fontColor": "#a074c4" }, "_elm_light": { - "fontCharacter": "\\E027", + "fontCharacter": "\\E028", "fontColor": "#498ba7" }, "_elm": { - "fontCharacter": "\\E027", + "fontCharacter": "\\E028", "fontColor": "#519aba" }, "_eslint_light": { - "fontCharacter": "\\E029", + "fontCharacter": "\\E02A", "fontColor": "#9068b0" }, "_eslint": { - "fontCharacter": "\\E029", + "fontCharacter": "\\E02A", "fontColor": "#a074c4" }, "_eslint_1_light": { - "fontCharacter": "\\E029", + "fontCharacter": "\\E02A", "fontColor": "#455155" }, "_eslint_1": { - "fontCharacter": "\\E029", + "fontCharacter": "\\E02A", "fontColor": "#4d5a5e" }, "_ethereum_light": { - "fontCharacter": "\\E02A", + "fontCharacter": "\\E02B", "fontColor": "#498ba7" }, "_ethereum": { - "fontCharacter": "\\E02A", + "fontCharacter": "\\E02B", "fontColor": "#519aba" }, "_f-sharp_light": { - "fontCharacter": "\\E02B", + "fontCharacter": "\\E02C", "fontColor": "#498ba7" }, "_f-sharp": { - "fontCharacter": "\\E02B", + "fontCharacter": "\\E02C", "fontColor": "#519aba" }, "_favicon_light": { - "fontCharacter": "\\E02C", + "fontCharacter": "\\E02D", "fontColor": "#b7b73b" }, "_favicon": { - "fontCharacter": "\\E02C", + "fontCharacter": "\\E02D", "fontColor": "#cbcb41" }, "_firebase_light": { - "fontCharacter": "\\E02D", + "fontCharacter": "\\E02E", "fontColor": "#cc6d2e" }, "_firebase": { - "fontCharacter": "\\E02D", + "fontCharacter": "\\E02E", "fontColor": "#e37933" }, "_firefox_light": { - "fontCharacter": "\\E02E", + "fontCharacter": "\\E02F", "fontColor": "#cc6d2e" }, "_firefox": { - "fontCharacter": "\\E02E", + "fontCharacter": "\\E02F", "fontColor": "#e37933" }, "_font_light": { - "fontCharacter": "\\E030", + "fontCharacter": "\\E031", "fontColor": "#b8383d" }, "_font": { - "fontCharacter": "\\E030", + "fontCharacter": "\\E031", "fontColor": "#cc3e44" }, "_git_light": { - "fontCharacter": "\\E031", + "fontCharacter": "\\E032", "fontColor": "#3b4b52" }, "_git": { - "fontCharacter": "\\E031", + "fontCharacter": "\\E032", "fontColor": "#41535b" }, "_go_light": { - "fontCharacter": "\\E035", + "fontCharacter": "\\E036", "fontColor": "#498ba7" }, "_go": { - "fontCharacter": "\\E035", + "fontCharacter": "\\E036", "fontColor": "#519aba" }, "_go2_light": { - "fontCharacter": "\\E036", + "fontCharacter": "\\E037", "fontColor": "#498ba7" }, "_go2": { - "fontCharacter": "\\E036", + "fontCharacter": "\\E037", "fontColor": "#519aba" }, "_gradle_light": { - "fontCharacter": "\\E037", - "fontColor": "#7fae42" + "fontCharacter": "\\E038", + "fontColor": "#498ba7" }, "_gradle": { - "fontCharacter": "\\E037", - "fontColor": "#8dc149" + "fontCharacter": "\\E038", + "fontColor": "#519aba" }, "_grails_light": { - "fontCharacter": "\\E038", + "fontCharacter": "\\E039", "fontColor": "#7fae42" }, "_grails": { - "fontCharacter": "\\E038", + "fontCharacter": "\\E039", "fontColor": "#8dc149" }, "_graphql_light": { - "fontCharacter": "\\E039", + "fontCharacter": "\\E03A", "fontColor": "#dd4b78" }, "_graphql": { - "fontCharacter": "\\E039", + "fontCharacter": "\\E03A", "fontColor": "#f55385" }, "_grunt_light": { - "fontCharacter": "\\E03A", + "fontCharacter": "\\E03B", "fontColor": "#cc6d2e" }, "_grunt": { - "fontCharacter": "\\E03A", + "fontCharacter": "\\E03B", "fontColor": "#e37933" }, "_gulp_light": { - "fontCharacter": "\\E03B", + "fontCharacter": "\\E03C", "fontColor": "#b8383d" }, "_gulp": { - "fontCharacter": "\\E03B", + "fontCharacter": "\\E03C", "fontColor": "#cc3e44" }, "_haml_light": { - "fontCharacter": "\\E03D", + "fontCharacter": "\\E03E", "fontColor": "#b8383d" }, "_haml": { - "fontCharacter": "\\E03D", + "fontCharacter": "\\E03E", "fontColor": "#cc3e44" }, "_happenings_light": { - "fontCharacter": "\\E03E", + "fontCharacter": "\\E03F", "fontColor": "#498ba7" }, "_happenings": { - "fontCharacter": "\\E03E", + "fontCharacter": "\\E03F", "fontColor": "#519aba" }, "_haskell_light": { - "fontCharacter": "\\E03F", + "fontCharacter": "\\E040", "fontColor": "#9068b0" }, "_haskell": { - "fontCharacter": "\\E03F", + "fontCharacter": "\\E040", "fontColor": "#a074c4" }, "_haxe_light": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E041", "fontColor": "#cc6d2e" }, "_haxe": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E041", "fontColor": "#e37933" }, "_haxe_1_light": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E041", "fontColor": "#b7b73b" }, "_haxe_1": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E041", "fontColor": "#cbcb41" }, "_haxe_2_light": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E041", "fontColor": "#498ba7" }, "_haxe_2": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E041", "fontColor": "#519aba" }, "_haxe_3_light": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E041", "fontColor": "#9068b0" }, "_haxe_3": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E041", "fontColor": "#a074c4" }, "_heroku_light": { - "fontCharacter": "\\E041", + "fontCharacter": "\\E042", "fontColor": "#9068b0" }, "_heroku": { - "fontCharacter": "\\E041", + "fontCharacter": "\\E042", "fontColor": "#a074c4" }, "_hex_light": { - "fontCharacter": "\\E042", + "fontCharacter": "\\E043", "fontColor": "#b8383d" }, "_hex": { - "fontCharacter": "\\E042", + "fontCharacter": "\\E043", "fontColor": "#cc3e44" }, "_html_light": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E044", "fontColor": "#498ba7" }, "_html": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E044", "fontColor": "#519aba" }, "_html_1_light": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E044", "fontColor": "#7fae42" }, "_html_1": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E044", "fontColor": "#8dc149" }, "_html_2_light": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E044", "fontColor": "#b7b73b" }, "_html_2": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E044", "fontColor": "#cbcb41" }, "_html_3_light": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E044", "fontColor": "#cc6d2e" }, "_html_3": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E044", "fontColor": "#e37933" }, "_html_erb_light": { - "fontCharacter": "\\E044", + "fontCharacter": "\\E045", "fontColor": "#b8383d" }, "_html_erb": { - "fontCharacter": "\\E044", + "fontCharacter": "\\E045", "fontColor": "#cc3e44" }, "_ignored_light": { - "fontCharacter": "\\E045", + "fontCharacter": "\\E046", "fontColor": "#3b4b52" }, "_ignored": { - "fontCharacter": "\\E045", + "fontCharacter": "\\E046", "fontColor": "#41535b" }, "_illustrator_light": { - "fontCharacter": "\\E046", + "fontCharacter": "\\E047", "fontColor": "#b7b73b" }, "_illustrator": { - "fontCharacter": "\\E046", + "fontCharacter": "\\E047", "fontColor": "#cbcb41" }, "_image_light": { - "fontCharacter": "\\E047", + "fontCharacter": "\\E048", "fontColor": "#9068b0" }, "_image": { - "fontCharacter": "\\E047", + "fontCharacter": "\\E048", "fontColor": "#a074c4" }, "_info_light": { - "fontCharacter": "\\E048", + "fontCharacter": "\\E049", "fontColor": "#498ba7" }, "_info": { - "fontCharacter": "\\E048", + "fontCharacter": "\\E049", "fontColor": "#519aba" }, "_ionic_light": { - "fontCharacter": "\\E049", + "fontCharacter": "\\E04A", "fontColor": "#498ba7" }, "_ionic": { - "fontCharacter": "\\E049", + "fontCharacter": "\\E04A", "fontColor": "#519aba" }, "_jade_light": { - "fontCharacter": "\\E04A", + "fontCharacter": "\\E04B", "fontColor": "#b8383d" }, "_jade": { - "fontCharacter": "\\E04A", + "fontCharacter": "\\E04B", "fontColor": "#cc3e44" }, "_java_light": { - "fontCharacter": "\\E04B", + "fontCharacter": "\\E04C", "fontColor": "#b8383d" }, "_java": { - "fontCharacter": "\\E04B", + "fontCharacter": "\\E04C", "fontColor": "#cc3e44" }, "_javascript_light": { - "fontCharacter": "\\E04C", + "fontCharacter": "\\E04D", "fontColor": "#b7b73b" }, "_javascript": { - "fontCharacter": "\\E04C", + "fontCharacter": "\\E04D", "fontColor": "#cbcb41" }, "_javascript_1_light": { - "fontCharacter": "\\E04C", + "fontCharacter": "\\E04D", "fontColor": "#cc6d2e" }, "_javascript_1": { - "fontCharacter": "\\E04C", + "fontCharacter": "\\E04D", "fontColor": "#e37933" }, "_javascript_2_light": { - "fontCharacter": "\\E04C", + "fontCharacter": "\\E04D", "fontColor": "#498ba7" }, "_javascript_2": { - "fontCharacter": "\\E04C", + "fontCharacter": "\\E04D", "fontColor": "#519aba" }, "_jenkins_light": { - "fontCharacter": "\\E04D", + "fontCharacter": "\\E04E", "fontColor": "#b8383d" }, "_jenkins": { - "fontCharacter": "\\E04D", + "fontCharacter": "\\E04E", "fontColor": "#cc3e44" }, "_jinja_light": { - "fontCharacter": "\\E04E", + "fontCharacter": "\\E04F", "fontColor": "#b8383d" }, "_jinja": { - "fontCharacter": "\\E04E", + "fontCharacter": "\\E04F", "fontColor": "#cc3e44" }, "_json_light": { - "fontCharacter": "\\E050", + "fontCharacter": "\\E051", "fontColor": "#b7b73b" }, "_json": { - "fontCharacter": "\\E050", + "fontCharacter": "\\E051", "fontColor": "#cbcb41" }, "_json_1_light": { - "fontCharacter": "\\E050", + "fontCharacter": "\\E051", "fontColor": "#7fae42" }, "_json_1": { - "fontCharacter": "\\E050", + "fontCharacter": "\\E051", "fontColor": "#8dc149" }, "_julia_light": { - "fontCharacter": "\\E051", + "fontCharacter": "\\E052", "fontColor": "#9068b0" }, "_julia": { - "fontCharacter": "\\E051", + "fontCharacter": "\\E052", "fontColor": "#a074c4" }, "_karma_light": { - "fontCharacter": "\\E052", + "fontCharacter": "\\E053", "fontColor": "#7fae42" }, "_karma": { - "fontCharacter": "\\E052", + "fontCharacter": "\\E053", "fontColor": "#8dc149" }, "_kotlin_light": { - "fontCharacter": "\\E053", + "fontCharacter": "\\E054", "fontColor": "#cc6d2e" }, "_kotlin": { - "fontCharacter": "\\E053", + "fontCharacter": "\\E054", "fontColor": "#e37933" }, "_less_light": { - "fontCharacter": "\\E054", + "fontCharacter": "\\E055", "fontColor": "#498ba7" }, "_less": { - "fontCharacter": "\\E054", + "fontCharacter": "\\E055", "fontColor": "#519aba" }, "_license_light": { - "fontCharacter": "\\E055", + "fontCharacter": "\\E056", "fontColor": "#b7b73b" }, "_license": { - "fontCharacter": "\\E055", + "fontCharacter": "\\E056", "fontColor": "#cbcb41" }, "_license_1_light": { - "fontCharacter": "\\E055", + "fontCharacter": "\\E056", "fontColor": "#cc6d2e" }, "_license_1": { - "fontCharacter": "\\E055", + "fontCharacter": "\\E056", "fontColor": "#e37933" }, "_license_2_light": { - "fontCharacter": "\\E055", + "fontCharacter": "\\E056", "fontColor": "#b8383d" }, "_license_2": { - "fontCharacter": "\\E055", + "fontCharacter": "\\E056", "fontColor": "#cc3e44" }, "_liquid_light": { - "fontCharacter": "\\E056", + "fontCharacter": "\\E057", "fontColor": "#7fae42" }, "_liquid": { - "fontCharacter": "\\E056", + "fontCharacter": "\\E057", "fontColor": "#8dc149" }, "_livescript_light": { - "fontCharacter": "\\E057", + "fontCharacter": "\\E058", "fontColor": "#498ba7" }, "_livescript": { - "fontCharacter": "\\E057", + "fontCharacter": "\\E058", "fontColor": "#519aba" }, "_lock_light": { - "fontCharacter": "\\E058", + "fontCharacter": "\\E059", "fontColor": "#7fae42" }, "_lock": { - "fontCharacter": "\\E058", + "fontCharacter": "\\E059", "fontColor": "#8dc149" }, "_lua_light": { - "fontCharacter": "\\E059", + "fontCharacter": "\\E05A", "fontColor": "#498ba7" }, "_lua": { - "fontCharacter": "\\E059", + "fontCharacter": "\\E05A", "fontColor": "#519aba" }, "_makefile_light": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E05B", "fontColor": "#cc6d2e" }, "_makefile": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E05B", "fontColor": "#e37933" }, "_makefile_1_light": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E05B", "fontColor": "#9068b0" }, "_makefile_1": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E05B", "fontColor": "#a074c4" }, "_makefile_2_light": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E05B", "fontColor": "#627379" }, "_makefile_2": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E05B", "fontColor": "#6d8086" }, "_makefile_3_light": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E05B", "fontColor": "#498ba7" }, "_makefile_3": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E05B", "fontColor": "#519aba" }, "_markdown_light": { - "fontCharacter": "\\E05B", + "fontCharacter": "\\E05C", "fontColor": "#498ba7" }, "_markdown": { - "fontCharacter": "\\E05B", + "fontCharacter": "\\E05C", "fontColor": "#519aba" }, "_maven_light": { - "fontCharacter": "\\E05C", + "fontCharacter": "\\E05D", "fontColor": "#b8383d" }, "_maven": { - "fontCharacter": "\\E05C", + "fontCharacter": "\\E05D", "fontColor": "#cc3e44" }, "_mdo_light": { - "fontCharacter": "\\E05D", + "fontCharacter": "\\E05E", "fontColor": "#b8383d" }, "_mdo": { - "fontCharacter": "\\E05D", + "fontCharacter": "\\E05E", "fontColor": "#cc3e44" }, "_mustache_light": { - "fontCharacter": "\\E05E", + "fontCharacter": "\\E05F", "fontColor": "#cc6d2e" }, "_mustache": { - "fontCharacter": "\\E05E", + "fontCharacter": "\\E05F", "fontColor": "#e37933" }, + "_nim_light": { + "fontCharacter": "\\E061", + "fontColor": "#b7b73b" + }, + "_nim": { + "fontCharacter": "\\E061", + "fontColor": "#cbcb41" + }, "_npm_light": { - "fontCharacter": "\\E060", + "fontCharacter": "\\E062", "fontColor": "#3b4b52" }, "_npm": { - "fontCharacter": "\\E060", + "fontCharacter": "\\E062", "fontColor": "#41535b" }, "_npm_1_light": { - "fontCharacter": "\\E060", + "fontCharacter": "\\E062", "fontColor": "#b8383d" }, "_npm_1": { - "fontCharacter": "\\E060", + "fontCharacter": "\\E062", "fontColor": "#cc3e44" }, "_npm_ignored_light": { - "fontCharacter": "\\E061", + "fontCharacter": "\\E063", "fontColor": "#3b4b52" }, "_npm_ignored": { - "fontCharacter": "\\E061", + "fontCharacter": "\\E063", "fontColor": "#41535b" }, "_nunjucks_light": { - "fontCharacter": "\\E062", + "fontCharacter": "\\E064", "fontColor": "#7fae42" }, "_nunjucks": { - "fontCharacter": "\\E062", + "fontCharacter": "\\E064", "fontColor": "#8dc149" }, "_ocaml_light": { - "fontCharacter": "\\E063", + "fontCharacter": "\\E065", "fontColor": "#cc6d2e" }, "_ocaml": { - "fontCharacter": "\\E063", + "fontCharacter": "\\E065", "fontColor": "#e37933" }, "_odata_light": { - "fontCharacter": "\\E064", + "fontCharacter": "\\E066", "fontColor": "#cc6d2e" }, "_odata": { - "fontCharacter": "\\E064", + "fontCharacter": "\\E066", "fontColor": "#e37933" }, "_pddl_light": { - "fontCharacter": "\\E065", + "fontCharacter": "\\E067", "fontColor": "#9068b0" }, "_pddl": { - "fontCharacter": "\\E065", + "fontCharacter": "\\E067", "fontColor": "#a074c4" }, "_pdf_light": { - "fontCharacter": "\\E066", + "fontCharacter": "\\E068", "fontColor": "#b8383d" }, "_pdf": { - "fontCharacter": "\\E066", + "fontCharacter": "\\E068", "fontColor": "#cc3e44" }, "_perl_light": { - "fontCharacter": "\\E067", + "fontCharacter": "\\E069", "fontColor": "#498ba7" }, "_perl": { - "fontCharacter": "\\E067", + "fontCharacter": "\\E069", "fontColor": "#519aba" }, "_photoshop_light": { - "fontCharacter": "\\E068", + "fontCharacter": "\\E06A", "fontColor": "#498ba7" }, "_photoshop": { - "fontCharacter": "\\E068", + "fontCharacter": "\\E06A", "fontColor": "#519aba" }, "_php_light": { - "fontCharacter": "\\E069", + "fontCharacter": "\\E06B", "fontColor": "#9068b0" }, "_php": { - "fontCharacter": "\\E069", + "fontCharacter": "\\E06B", "fontColor": "#a074c4" }, "_plan_light": { - "fontCharacter": "\\E06A", + "fontCharacter": "\\E06C", "fontColor": "#7fae42" }, "_plan": { - "fontCharacter": "\\E06A", + "fontCharacter": "\\E06C", "fontColor": "#8dc149" }, "_platformio_light": { - "fontCharacter": "\\E06B", + "fontCharacter": "\\E06D", "fontColor": "#cc6d2e" }, "_platformio": { - "fontCharacter": "\\E06B", + "fontCharacter": "\\E06D", "fontColor": "#e37933" }, "_powershell_light": { - "fontCharacter": "\\E06C", + "fontCharacter": "\\E06E", "fontColor": "#498ba7" }, "_powershell": { - "fontCharacter": "\\E06C", + "fontCharacter": "\\E06E", "fontColor": "#519aba" }, "_prolog_light": { - "fontCharacter": "\\E06E", + "fontCharacter": "\\E070", "fontColor": "#cc6d2e" }, "_prolog": { - "fontCharacter": "\\E06E", + "fontCharacter": "\\E070", "fontColor": "#e37933" }, "_pug_light": { - "fontCharacter": "\\E06F", + "fontCharacter": "\\E071", "fontColor": "#b8383d" }, "_pug": { - "fontCharacter": "\\E06F", + "fontCharacter": "\\E071", "fontColor": "#cc3e44" }, "_puppet_light": { - "fontCharacter": "\\E070", + "fontCharacter": "\\E072", "fontColor": "#b7b73b" }, "_puppet": { - "fontCharacter": "\\E070", + "fontCharacter": "\\E072", "fontColor": "#cbcb41" }, "_python_light": { - "fontCharacter": "\\E071", + "fontCharacter": "\\E073", "fontColor": "#498ba7" }, "_python": { - "fontCharacter": "\\E071", + "fontCharacter": "\\E073", "fontColor": "#519aba" }, "_react_light": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E075", "fontColor": "#498ba7" }, "_react": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E075", "fontColor": "#519aba" }, "_react_1_light": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E075", "fontColor": "#cc6d2e" }, "_react_1": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E075", "fontColor": "#e37933" }, "_react_2_light": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E075", "fontColor": "#b7b73b" }, "_react_2": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E075", "fontColor": "#cbcb41" }, "_reasonml_light": { - "fontCharacter": "\\E074", + "fontCharacter": "\\E076", "fontColor": "#b8383d" }, "_reasonml": { - "fontCharacter": "\\E074", + "fontCharacter": "\\E076", "fontColor": "#cc3e44" }, "_rollup_light": { - "fontCharacter": "\\E075", + "fontCharacter": "\\E077", "fontColor": "#b8383d" }, "_rollup": { - "fontCharacter": "\\E075", + "fontCharacter": "\\E077", "fontColor": "#cc3e44" }, "_ruby_light": { - "fontCharacter": "\\E076", + "fontCharacter": "\\E078", "fontColor": "#b8383d" }, "_ruby": { - "fontCharacter": "\\E076", + "fontCharacter": "\\E078", "fontColor": "#cc3e44" }, "_rust_light": { - "fontCharacter": "\\E077", + "fontCharacter": "\\E079", "fontColor": "#627379" }, "_rust": { - "fontCharacter": "\\E077", + "fontCharacter": "\\E079", "fontColor": "#6d8086" }, "_salesforce_light": { - "fontCharacter": "\\E078", + "fontCharacter": "\\E07A", "fontColor": "#498ba7" }, "_salesforce": { - "fontCharacter": "\\E078", + "fontCharacter": "\\E07A", "fontColor": "#519aba" }, "_sass_light": { - "fontCharacter": "\\E079", + "fontCharacter": "\\E07B", "fontColor": "#dd4b78" }, "_sass": { - "fontCharacter": "\\E079", + "fontCharacter": "\\E07B", "fontColor": "#f55385" }, "_sbt_light": { - "fontCharacter": "\\E07A", + "fontCharacter": "\\E07C", "fontColor": "#498ba7" }, "_sbt": { - "fontCharacter": "\\E07A", + "fontCharacter": "\\E07C", "fontColor": "#519aba" }, "_scala_light": { - "fontCharacter": "\\E07B", + "fontCharacter": "\\E07D", "fontColor": "#b8383d" }, "_scala": { - "fontCharacter": "\\E07B", + "fontCharacter": "\\E07D", "fontColor": "#cc3e44" }, "_shell_light": { - "fontCharacter": "\\E07E", + "fontCharacter": "\\E080", "fontColor": "#455155" }, "_shell": { - "fontCharacter": "\\E07E", + "fontCharacter": "\\E080", "fontColor": "#4d5a5e" }, "_slim_light": { - "fontCharacter": "\\E07F", + "fontCharacter": "\\E081", "fontColor": "#cc6d2e" }, "_slim": { - "fontCharacter": "\\E07F", + "fontCharacter": "\\E081", "fontColor": "#e37933" }, "_smarty_light": { - "fontCharacter": "\\E080", + "fontCharacter": "\\E082", "fontColor": "#b7b73b" }, "_smarty": { - "fontCharacter": "\\E080", + "fontCharacter": "\\E082", "fontColor": "#cbcb41" }, "_spring_light": { - "fontCharacter": "\\E081", + "fontCharacter": "\\E083", "fontColor": "#7fae42" }, "_spring": { - "fontCharacter": "\\E081", + "fontCharacter": "\\E083", "fontColor": "#8dc149" }, "_stylelint_light": { - "fontCharacter": "\\E082", + "fontCharacter": "\\E084", "fontColor": "#bfc2c1" }, "_stylelint": { - "fontCharacter": "\\E082", + "fontCharacter": "\\E084", "fontColor": "#d4d7d6" }, "_stylelint_1_light": { - "fontCharacter": "\\E082", + "fontCharacter": "\\E084", "fontColor": "#455155" }, "_stylelint_1": { - "fontCharacter": "\\E082", + "fontCharacter": "\\E084", "fontColor": "#4d5a5e" }, "_stylus_light": { - "fontCharacter": "\\E083", + "fontCharacter": "\\E085", "fontColor": "#7fae42" }, "_stylus": { - "fontCharacter": "\\E083", + "fontCharacter": "\\E085", "fontColor": "#8dc149" }, "_sublime_light": { - "fontCharacter": "\\E084", + "fontCharacter": "\\E086", "fontColor": "#cc6d2e" }, "_sublime": { - "fontCharacter": "\\E084", + "fontCharacter": "\\E086", "fontColor": "#e37933" }, "_svg_light": { - "fontCharacter": "\\E085", + "fontCharacter": "\\E087", "fontColor": "#9068b0" }, "_svg": { - "fontCharacter": "\\E085", + "fontCharacter": "\\E087", "fontColor": "#a074c4" }, "_svg_1_light": { - "fontCharacter": "\\E085", + "fontCharacter": "\\E087", "fontColor": "#498ba7" }, "_svg_1": { - "fontCharacter": "\\E085", + "fontCharacter": "\\E087", "fontColor": "#519aba" }, "_swift_light": { - "fontCharacter": "\\E086", + "fontCharacter": "\\E088", "fontColor": "#cc6d2e" }, "_swift": { - "fontCharacter": "\\E086", + "fontCharacter": "\\E088", "fontColor": "#e37933" }, "_terraform_light": { - "fontCharacter": "\\E087", + "fontCharacter": "\\E089", "fontColor": "#9068b0" }, "_terraform": { - "fontCharacter": "\\E087", + "fontCharacter": "\\E089", "fontColor": "#a074c4" }, "_tex_light": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E08A", "fontColor": "#498ba7" }, "_tex": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E08A", "fontColor": "#519aba" }, "_tex_1_light": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E08A", "fontColor": "#b7b73b" }, "_tex_1": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E08A", "fontColor": "#cbcb41" }, "_tex_2_light": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E08A", "fontColor": "#cc6d2e" }, "_tex_2": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E08A", "fontColor": "#e37933" }, "_tex_3_light": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E08A", "fontColor": "#bfc2c1" }, "_tex_3": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E08A", "fontColor": "#d4d7d6" }, "_todo": { - "fontCharacter": "\\E08A" + "fontCharacter": "\\E08C" }, "_tsconfig_light": { - "fontCharacter": "\\E08B", + "fontCharacter": "\\E08D", "fontColor": "#498ba7" }, "_tsconfig": { - "fontCharacter": "\\E08B", + "fontCharacter": "\\E08D", "fontColor": "#519aba" }, "_twig_light": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08E", "fontColor": "#7fae42" }, "_twig": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08E", "fontColor": "#8dc149" }, "_typescript_light": { - "fontCharacter": "\\E08D", + "fontCharacter": "\\E08F", "fontColor": "#498ba7" }, "_typescript": { - "fontCharacter": "\\E08D", + "fontCharacter": "\\E08F", "fontColor": "#519aba" }, "_typescript_1_light": { - "fontCharacter": "\\E08D", + "fontCharacter": "\\E08F", "fontColor": "#b7b73b" }, "_typescript_1": { - "fontCharacter": "\\E08D", + "fontCharacter": "\\E08F", "fontColor": "#cbcb41" }, "_vala_light": { - "fontCharacter": "\\E08E", + "fontCharacter": "\\E090", "fontColor": "#627379" }, "_vala": { - "fontCharacter": "\\E08E", + "fontCharacter": "\\E090", "fontColor": "#6d8086" }, "_video_light": { - "fontCharacter": "\\E08F", + "fontCharacter": "\\E091", "fontColor": "#dd4b78" }, "_video": { - "fontCharacter": "\\E08F", + "fontCharacter": "\\E091", "fontColor": "#f55385" }, "_vue_light": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E092", "fontColor": "#7fae42" }, "_vue": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E092", "fontColor": "#8dc149" }, "_wasm_light": { - "fontCharacter": "\\E091", + "fontCharacter": "\\E093", "fontColor": "#9068b0" }, "_wasm": { - "fontCharacter": "\\E091", + "fontCharacter": "\\E093", "fontColor": "#a074c4" }, "_wat_light": { - "fontCharacter": "\\E092", + "fontCharacter": "\\E094", "fontColor": "#9068b0" }, "_wat": { - "fontCharacter": "\\E092", + "fontCharacter": "\\E094", "fontColor": "#a074c4" }, "_webpack_light": { - "fontCharacter": "\\E093", + "fontCharacter": "\\E095", "fontColor": "#498ba7" }, "_webpack": { - "fontCharacter": "\\E093", + "fontCharacter": "\\E095", "fontColor": "#519aba" }, "_wgt_light": { - "fontCharacter": "\\E094", + "fontCharacter": "\\E096", "fontColor": "#498ba7" }, "_wgt": { - "fontCharacter": "\\E094", + "fontCharacter": "\\E096", "fontColor": "#519aba" }, "_windows_light": { - "fontCharacter": "\\E095", + "fontCharacter": "\\E097", "fontColor": "#498ba7" }, "_windows": { - "fontCharacter": "\\E095", + "fontCharacter": "\\E097", "fontColor": "#519aba" }, "_word_light": { - "fontCharacter": "\\E096", + "fontCharacter": "\\E098", "fontColor": "#498ba7" }, "_word": { - "fontCharacter": "\\E096", + "fontCharacter": "\\E098", "fontColor": "#519aba" }, "_xls_light": { - "fontCharacter": "\\E097", + "fontCharacter": "\\E099", "fontColor": "#7fae42" }, "_xls": { - "fontCharacter": "\\E097", + "fontCharacter": "\\E099", "fontColor": "#8dc149" }, "_xml_light": { - "fontCharacter": "\\E098", + "fontCharacter": "\\E09A", "fontColor": "#cc6d2e" }, "_xml": { - "fontCharacter": "\\E098", + "fontCharacter": "\\E09A", "fontColor": "#e37933" }, "_yarn_light": { - "fontCharacter": "\\E099", + "fontCharacter": "\\E09B", "fontColor": "#498ba7" }, "_yarn": { - "fontCharacter": "\\E099", + "fontCharacter": "\\E09B", "fontColor": "#519aba" }, "_yml_light": { - "fontCharacter": "\\E09A", + "fontCharacter": "\\E09C", "fontColor": "#9068b0" }, "_yml": { - "fontCharacter": "\\E09A", + "fontCharacter": "\\E09C", "fontColor": "#a074c4" }, "_zip_light": { - "fontCharacter": "\\E09B", + "fontCharacter": "\\E09D", "fontColor": "#b8383d" }, "_zip": { - "fontCharacter": "\\E09B", + "fontCharacter": "\\E09D", "fontColor": "#cc3e44" }, "_zip_1_light": { - "fontCharacter": "\\E09B", + "fontCharacter": "\\E09D", "fontColor": "#627379" }, "_zip_1": { - "fontCharacter": "\\E09B", + "fontCharacter": "\\E09D", "fontColor": "#6d8086" } }, @@ -1384,6 +1408,7 @@ "hh": "_cpp_1", "hpp": "_cpp_1", "hxx": "_cpp_1", + "h++": "_cpp_1", "edn": "_clojure_1", "cfc": "_coldfusion", "cfm": "_coldfusion", @@ -1400,6 +1425,9 @@ "csv": "_csv", "xls": "_xls", "xlsx": "_xls", + "cu": "_cu", + "cuh": "_cu_1", + "hu": "_cu_1", "cake": "_cake", "ctp": "_cake_php", "d": "_d", @@ -1428,6 +1456,7 @@ "hxs": "_haxe_1", "hxp": "_haxe_2", "hxml": "_haxe_3", + "jade": "_jade", "class": "_java", "classpath": "_java", "properties": "_java", @@ -1449,6 +1478,8 @@ "ad": "_argdown", "mustache": "_mustache", "stache": "_mustache", + "nim": "_nim", + "nims": "_nim", "njk": "_nunjucks", "nunjucks": "_nunjucks", "nunjs": "_nunjucks", @@ -1456,8 +1487,6 @@ "njs": "_nunjucks", "nj": "_nunjucks", "npm-debug.log": "_npm", - "npmignore": "_npm_1", - "npmrc": "_npm_1", "ml": "_ocaml", "mli": "_ocaml", "cmx": "_ocaml", @@ -1467,7 +1496,6 @@ "pddl": "_pddl", "plan": "_plan", "happenings": "_happenings", - "pug": "_pug", "pp": "_puppet", "epp": "_puppet", "spec.jsx": "_react_1", @@ -1485,6 +1513,7 @@ "springbeans": "_spring", "slim": "_slim", "smarty.tpl": "_smarty", + "tpl": "_smarty", "sbt": "_sbt", "scala": "_scala", "sol": "_ethereum", @@ -1523,6 +1552,8 @@ "pxm": "_image", "svg": "_svg", "svgx": "_image", + "tiff": "_image", + "webp": "_image", "sublime-project": "_sublime", "sublime-workspace": "_sublime", "component": "_salesforce", @@ -1642,11 +1673,13 @@ "csharp": "_c-sharp", "css": "_css", "dockerfile": "_docker", + "ignore": "_npm_1", "fsharp": "_f-sharp", "go": "_go2", "groovy": "_grails", "handlebars": "_mustache", "html": "_html_3", + "properties": "_npm_1", "java": "_java", "javascriptreact": "_react", "javascript": "_javascript", @@ -1660,7 +1693,7 @@ "perl": "_perl", "php": "_php", "powershell": "_powershell", - "jade": "_jade", + "jade": "_pug", "python": "_python", "r": "_R", "razor": "_html", @@ -1712,6 +1745,7 @@ "hh": "_cpp_1_light", "hpp": "_cpp_1_light", "hxx": "_cpp_1_light", + "h++": "_cpp_1_light", "edn": "_clojure_1_light", "cfc": "_coldfusion_light", "cfm": "_coldfusion_light", @@ -1728,6 +1762,9 @@ "csv": "_csv_light", "xls": "_xls_light", "xlsx": "_xls_light", + "cu": "_cu_light", + "cuh": "_cu_1_light", + "hu": "_cu_1_light", "cake": "_cake_light", "ctp": "_cake_php_light", "d": "_d_light", @@ -1756,6 +1793,7 @@ "hxs": "_haxe_1_light", "hxp": "_haxe_2_light", "hxml": "_haxe_3_light", + "jade": "_jade_light", "class": "_java_light", "classpath": "_java_light", "properties": "_java_light", @@ -1777,6 +1815,8 @@ "ad": "_argdown_light", "mustache": "_mustache_light", "stache": "_mustache_light", + "nim": "_nim_light", + "nims": "_nim_light", "njk": "_nunjucks_light", "nunjucks": "_nunjucks_light", "nunjs": "_nunjucks_light", @@ -1784,8 +1824,6 @@ "njs": "_nunjucks_light", "nj": "_nunjucks_light", "npm-debug.log": "_npm_light", - "npmignore": "_npm_1_light", - "npmrc": "_npm_1_light", "ml": "_ocaml_light", "mli": "_ocaml_light", "cmx": "_ocaml_light", @@ -1795,7 +1833,6 @@ "pddl": "_pddl_light", "plan": "_plan_light", "happenings": "_happenings_light", - "pug": "_pug_light", "pp": "_puppet_light", "epp": "_puppet_light", "spec.jsx": "_react_1_light", @@ -1813,6 +1850,7 @@ "springbeans": "_spring_light", "slim": "_slim_light", "smarty.tpl": "_smarty_light", + "tpl": "_smarty_light", "sbt": "_sbt_light", "scala": "_scala_light", "sol": "_ethereum_light", @@ -1851,6 +1889,8 @@ "pxm": "_image_light", "svg": "_svg_light", "svgx": "_image_light", + "tiff": "_image_light", + "webp": "_image_light", "sublime-project": "_sublime_light", "sublime-workspace": "_sublime_light", "component": "_salesforce_light", @@ -1913,11 +1953,13 @@ "csharp": "_c-sharp_light", "css": "_css_light", "dockerfile": "_docker_light", + "ignore": "_npm_1_light", "fsharp": "_f-sharp_light", "go": "_go2_light", "groovy": "_grails_light", "handlebars": "_mustache_light", "html": "_html_3_light", + "properties": "_npm_1_light", "java": "_java_light", "javascriptreact": "_react_light", "javascript": "_javascript_light", @@ -1931,7 +1973,7 @@ "perl": "_perl_light", "php": "_php_light", "powershell": "_powershell_light", - "jade": "_jade_light", + "jade": "_pug_light", "python": "_python_light", "r": "_R_light", "razor": "_html_light", @@ -2024,5 +2066,5 @@ "npm-debug.log": "_npm_ignored_light" } }, - "version": "https://github.com/jesseweed/seti-ui/commit/f3b2775662b0075aab56e5f0c03269f21f3f0f30" + "version": "https://github.com/jesseweed/seti-ui/commit/719e5d384e878b0e190abc80247a8726f083a393" } \ No newline at end of file diff --git a/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json b/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json index b23ff8bb85c..eaf90258d35 100644 --- a/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json +++ b/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json @@ -270,6 +270,20 @@ "foreground": "#D33682" } }, + { + "name": "Markup: Strong", + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Emphasis", + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, { "name": "Markup Inline", "scope": "markup.inline.raw", @@ -282,6 +296,7 @@ "name": "Markup Headings", "scope": "markup.heading", "settings": { + "fontStyle": "bold", "foreground": "#268BD2" } }, diff --git a/extensions/theme-solarized-light/themes/solarized-light-color-theme.json b/extensions/theme-solarized-light/themes/solarized-light-color-theme.json index 21f530d00a3..77aa0f29079 100644 --- a/extensions/theme-solarized-light/themes/solarized-light-color-theme.json +++ b/extensions/theme-solarized-light/themes/solarized-light-color-theme.json @@ -273,6 +273,20 @@ "foreground": "#D33682" } }, + { + "name": "Markup: Strong", + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Emphasis", + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, { "name": "Markup Inline", "scope": "markup.inline.raw", @@ -285,6 +299,7 @@ "name": "Markup Headings", "scope": "markup.heading", "settings": { + "fontStyle": "bold", "foreground": "#268BD2" } }, diff --git a/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-theme.json b/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-theme.json index 0baee6822ef..bdccdb49d91 100644 --- a/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-theme.json +++ b/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-theme.json @@ -223,6 +223,20 @@ "foreground": "#FFC58F" } }, + { + "name": "Markup: Strong", + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Emphasis", + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, { "name": "Markup Inline", "scope": "markup.inline.raw", @@ -231,6 +245,13 @@ "foreground": "#FF9DA4" } }, + { + "name": "Markup Headings", + "scope": "markup.heading", + "settings": { + "fontStyle": "bold" + } + }, { "scope": "token.info-token", "settings": { diff --git a/extensions/typescript-basics/snippets/typescript.code-snippets b/extensions/typescript-basics/snippets/typescript.code-snippets index 0587884ee12..8eeb13e2e2d 100644 --- a/extensions/typescript-basics/snippets/typescript.code-snippets +++ b/extensions/typescript-basics/snippets/typescript.code-snippets @@ -167,16 +167,16 @@ "}" ], "description": "For-Of Loop" - }, - "For-Await-Of Loop": { - "prefix": "forawaitof", - "body": [ + }, + "For-Await-Of Loop": { + "prefix": "forawaitof", + "body": [ "for await (const ${1:iterator} of ${2:object}) {", "\t$0", "}" ], - "description": "For-Await-Of Loop" - }, + "description": "For-Await-Of Loop" + }, "Function Statement": { "prefix": "function", "body": [ @@ -278,5 +278,32 @@ "//#endregion" ], "description": "Folding Region End" + }, + "new Promise": { + "prefix": "newpromise", + "body": [ + "new Promise<$1:type>((resolve, reject) => {", + "\t$1", + "})" + ], + "description": "Create a new Promise" + }, + "Async Function Statement": { + "prefix": "async function", + "body": [ + "async function ${1:name}(${2:params}:${3:type}) {", + "\t$0", + "}" + ], + "description": "Async Function Statement" + }, + "Async Function Expression": { + "prefix": "async arrow function", + "body": [ + "async (${1:params}:${2:type}) => {", + "\t$0", + "}" + ], + "description": "Async Function Expression" } } diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 4322984e099..6009e367122 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -9,14 +9,14 @@ "typescript.disableAutomaticTypeAcquisition": "Disables automatic type acquisition. Automatic type acquisition fetches `@types` packages from npm to improve IntelliSense for external libraries.", "typescript.enablePromptUseWorkspaceTsdk": "Enables prompting of users to use the TypeScript version configured in the workspace for Intellisense.", "typescript.tsserver.log": "Enables logging of the TS server to a file. This log can be used to diagnose TS Server issues. The log may contain file paths, source code, and other potentially sensitive information from your project.", - "typescript.tsserver.pluginPaths": "Additional paths to discover TypeScript Language Service plugins. Requires using TypeScript 2.3.0 or newer in the workspace.", + "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.validate.enable": "Enable/disable TypeScript validation.", "typescript.format.enable": "Enable/disable default TypeScript formatter.", "javascript.format.enable": "Enable/disable default JavaScript formatter.", "format.insertSpaceAfterCommaDelimiter": "Defines space handling after a comma delimiter.", - "format.insertSpaceAfterConstructor": "Defines space handling after the constructor keyword. Requires using TypeScript 2.3.0 or newer in the workspace.", + "format.insertSpaceAfterConstructor": "Defines space handling after the constructor keyword.", "format.insertSpaceAfterSemicolonInForStatements": "Defines space handling after a semicolon in a for statement.", "format.insertSpaceBeforeAndAfterBinaryOperators": "Defines space handling after a binary operator.", "format.insertSpaceAfterKeywordsInControlFlowStatements": "Defines space handling after keywords in a control flow statement.", @@ -24,10 +24,10 @@ "format.insertSpaceBeforeFunctionParenthesis": "Defines space handling before function argument parentheses.", "format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": "Defines space handling after opening and before closing non-empty parenthesis.", "format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": "Defines space handling after opening and before closing non-empty brackets.", - "format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": "Defines space handling after opening and before closing non-empty braces. Requires using TypeScript 2.3.0 or newer in the workspace.", + "format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": "Defines space handling after opening and before closing non-empty braces.", "format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": "Defines space handling after opening and before closing template string braces.", "format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": "Defines space handling after opening and before closing JSX expression braces.", - "format.insertSpaceAfterTypeAssertion": "Defines space handling after type assertions in TypeScript. Requires using TypeScript 2.4 or newer in the workspace.", + "format.insertSpaceAfterTypeAssertion": "Defines space handling after type assertions in TypeScript.", "format.placeOpenBraceOnNewLineForFunctions": "Defines whether an open brace is put onto a new line for functions or not.", "format.placeOpenBraceOnNewLineForControlBlocks": "Defines whether an open brace is put onto a new line for control blocks or not.", "format.semicolons": "Defines handling of optional semicolons. Requires using TypeScript 3.7 or newer in the workspace.", @@ -45,8 +45,8 @@ "typescript.restartTsServer": "Restart TS server", "typescript.selectTypeScriptVersion.title": "Select TypeScript Version...", "typescript.reportStyleChecksAsWarnings": "Report style checks as warnings.", - "javascript.implicitProjectConfig.checkJs": "Enable/disable semantic checking of JavaScript files. Existing jsconfig.json or tsconfig.json files override this setting. Requires using TypeScript 2.3.1 or newer in the workspace.", - "typescript.npm": "Specifies the path to the npm executable used for Automatic Type Acquisition. Requires using TypeScript 2.3.4 or newer in the workspace.", + "javascript.implicitProjectConfig.checkJs": "Enable/disable semantic checking of JavaScript files. Existing jsconfig.json or tsconfig.json files override this setting.", + "typescript.npm": "Specifies the path to the npm executable used for Automatic Type Acquisition.", "typescript.check.npmIsInstalled": "Check if npm is installed for Automatic Type Acquisition.", "configuration.suggest.names": "Enable/disable including unique names from the file in JavaScript suggestions. Note that name suggestions are always disabled in JavaScript code that is semantically checked using `@ts-check` or `checkJs`.", "typescript.tsc.autoDetect": "Controls auto detection of tsc tasks.", @@ -60,13 +60,13 @@ "configuration.tsserver.useSeparateSyntaxServer": "Enable/disable spawning a separate TypeScript server that can more quickly respond to syntax related operations, such as calculating folding or computing document symbols. Requires using TypeScript 3.4.0 or newer in the workspace.", "configuration.tsserver.maxTsServerMemory": "Set the maximum amount of memory (in MB) to allocate to the TypeScript server process", "configuration.tsserver.experimental.enableProjectDiagnostics": "(Experimental) Enables project wide error reporting.", - "typescript.locale": "Sets the locale used to report JavaScript and TypeScript errors. Requires using TypeScript 2.6.0 or newer in the workspace. Default of `null` uses VS Code's locale.", - "javascript.implicitProjectConfig.experimentalDecorators": "Enable/disable `experimentalDecorators` for JavaScript files that are not part of a project. Existing jsconfig.json or tsconfig.json files override this setting. Requires using TypeScript 2.3.1 or newer in the workspace.", - "configuration.suggest.autoImports": "Enable/disable auto import suggestions. Requires using TypeScript 2.6.1 or newer in the workspace.", + "typescript.locale": "Sets the locale used to report JavaScript and TypeScript errors. Default of `null` uses VS Code's locale.", + "javascript.implicitProjectConfig.experimentalDecorators": "Enable/disable `experimentalDecorators` for JavaScript files that are not part of a project. Existing jsconfig.json or tsconfig.json files override this setting.", + "configuration.suggest.autoImports": "Enable/disable auto import suggestions.", "taskDefinition.tsconfig.description": "The tsconfig file that defines the TS build.", - "javascript.suggestionActions.enabled": "Enable/disable suggestion diagnostics for JavaScript files in the editor. Requires using TypeScript 2.8 or newer in the workspace.", - "typescript.suggestionActions.enabled": "Enable/disable suggestion diagnostics for TypeScript files in the editor. Requires using TypeScript 2.8 or newer in the workspace.", - "typescript.preferences.quoteStyle": "Preferred quote style to use for quick fixes: `single` quotes, `double` quotes, or `auto` infer quote type from existing imports. Requires using TypeScript 2.9 or newer in the workspace.", + "javascript.suggestionActions.enabled": "Enable/disable suggestion diagnostics for JavaScript files in the editor.", + "typescript.suggestionActions.enabled": "Enable/disable suggestion diagnostics for TypeScript files in the editor.", + "typescript.preferences.quoteStyle": "Preferred quote style to use for quick fixes: `single` quotes, `double` quotes, or `auto` infer quote type from existing imports.", "typescript.preferences.importModuleSpecifier": "Preferred path style for auto imports.", "typescript.preferences.importModuleSpecifier.auto": "Automatically select import path style. Prefers using a relative import if `baseUrl` is configured and the relative path has fewer segments than the non-relative import.", "typescript.preferences.importModuleSpecifier.relative": "Relative to the file location.", @@ -80,11 +80,11 @@ "typescript.preferences.includePackageJsonAutoImports.auto": "Search dependencies based on estimated performance impact.", "typescript.preferences.includePackageJsonAutoImports.on": "Always search dependencies.", "typescript.preferences.includePackageJsonAutoImports.off": "Never search dependencies.", - "typescript.updateImportsOnFileMove.enabled": "Enable/disable automatic updating of import paths when you rename or move a file in VS Code. Requires using TypeScript 2.9 or newer in the workspace.", + "typescript.updateImportsOnFileMove.enabled": "Enable/disable automatic updating of import paths when you rename or move a file in VS Code.", "typescript.updateImportsOnFileMove.enabled.prompt": "Prompt on each rename.", "typescript.updateImportsOnFileMove.enabled.always": "Always update paths automatically.", "typescript.updateImportsOnFileMove.enabled.never": "Never rename paths and don't prompt.", - "typescript.autoClosingTags": "Enable/disable automatic closing of JSX tags. Requires using TypeScript 3.0 or newer in the workspace.", + "typescript.autoClosingTags": "Enable/disable automatic closing of JSX tags.", "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.", diff --git a/extensions/typescript-language-features/src/extension.browser.ts b/extensions/typescript-language-features/src/extension.browser.ts index 9291e22ae36..6a0f97241ea 100644 --- a/extensions/typescript-language-features/src/extension.browser.ts +++ b/extensions/typescript-language-features/src/extension.browser.ts @@ -52,7 +52,7 @@ export function activate( const versionProvider = new StaticVersionProvider( new TypeScriptVersion( TypeScriptVersionSource.Bundled, - context.asAbsolutePath('dist/browser/typescript-web/tsserver.web.js'), + vscode.Uri.joinPath(context.extensionUri, 'dist/browser/typescript-web/tsserver.web.js').toString(), API.v400)); const lazyClientHost = createLazyClientHost(context, false, { @@ -78,4 +78,3 @@ export function activate( return getExtensionApi(onCompletionAccepted.event, pluginManager); } - diff --git a/extensions/typescript-language-features/src/languageFeatures/completions.ts b/extensions/typescript-language-features/src/languageFeatures/completions.ts index ae827b95b14..96914bc83a7 100644 --- a/extensions/typescript-language-features/src/languageFeatures/completions.ts +++ b/extensions/typescript-language-features/src/languageFeatures/completions.ts @@ -338,14 +338,13 @@ class CompletionAcceptedCommand implements Command { if (item instanceof MyCompletionItem) { /* __GDPR__ "completions.accept" : { - "isPackageJsonImport" : { "classification": "SystemMetadata", "purpose": "FeatureInsight" }, + "isPackageJsonImport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "${include}": [ "${TypeScriptCommonProperties}" ] } */ this.telemetryReporter.logTelemetry('completions.accept', { - // @ts-expect-error - remove after TS 4.0 protocol update isPackageJsonImport: item.tsEntry.isPackageJsonImport ? 'true' : undefined, }); } @@ -540,7 +539,6 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< for (let entry of entries) { if (!shouldExcludeCompletionEntry(entry, completionConfiguration)) { items.push(new MyCompletionItem(position, document, entry, completionContext, metadata)); - // @ts-expect-error - remove after TS 4.0 protocol update includesPackageJsonImport = !!entry.isPackageJsonImport; } } @@ -557,12 +555,12 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< ) { /* __GDPR__ "completions.execute" : { - "duration" : { "classification": "SystemMetadata", "purpose": "FeatureInsight" }, - "type" : { "classification": "SystemMetadata", "purpose": "FeatureInsight" }, - "count" : { "classification": "SystemMetadata", "purpose": "FeatureInsight" }, - "updateGraphDurationMs" : { "classification": "SystemMetadata", "purpose": "FeatureInsight" }, - "createAutoImportProviderProgramDurationMs" : { "classification": "SystemMetadata", "purpose": "FeatureInsight" }, - "includesPackageJsonImport" : { "classification": "SystemMetadata", "purpose": "FeatureInsight" }, + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "updateGraphDurationMs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "createAutoImportProviderProgramDurationMs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "includesPackageJsonImport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "${include}": [ "${TypeScriptCommonProperties}" ] @@ -643,7 +641,11 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< const { snippet, parameterCount } = snippetForFunctionCall(item, detail.displayParts); item.insertText = snippet; if (parameterCount > 0) { - commands.push({ title: 'triggerParameterHints', command: 'editor.action.triggerParameterHints' }); + //Fix for https://github.com/microsoft/vscode/issues/104059 + //Don't show parameter hints if "editor.parameterHints.enabled": false + if (vscode.workspace.getConfiguration('editor.parameterHints').get('enabled')) { + commands.push({ title: 'triggerParameterHints', command: 'editor.action.triggerParameterHints' }); + } } } } diff --git a/extensions/typescript-language-features/src/languageFeatures/definitions.ts b/extensions/typescript-language-features/src/languageFeatures/definitions.ts index 7c01400c206..6d6e4fc2851 100644 --- a/extensions/typescript-language-features/src/languageFeatures/definitions.ts +++ b/extensions/typescript-language-features/src/languageFeatures/definitions.ts @@ -39,10 +39,10 @@ export default class TypeScriptDefinitionProvider extends DefinitionProviderBase return response.body.definitions .map((location): vscode.DefinitionLink => { const target = typeConverters.Location.fromTextSpan(this.client.toResource(location.file), location); - if ((location as any).contextStart) { + if (location.contextStart && location.contextEnd) { return { originSelectionRange: span, - targetRange: typeConverters.Range.fromLocations((location as any).contextStart, (location as any).contextEnd), + targetRange: typeConverters.Range.fromLocations(location.contextStart, location.contextEnd), targetUri: target.uri, targetSelectionRange: target.range, }; diff --git a/extensions/typescript-language-features/src/languageFeatures/hover.ts b/extensions/typescript-language-features/src/languageFeatures/hover.ts index c6c4860f663..a4de074897f 100644 --- a/extensions/typescript-language-features/src/languageFeatures/hover.ts +++ b/extensions/typescript-language-features/src/languageFeatures/hover.ts @@ -5,7 +5,8 @@ import * as vscode from 'vscode'; import type * as Proto from '../protocol'; -import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; +import { localize } from '../tsServer/versionProvider'; +import { ClientCapability, ITypeScriptServiceClient, ServerType } from '../typescriptService'; import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration'; import { DocumentSelector } from '../utils/documentSelector'; import { markdownDocumentation } from '../utils/previewer'; @@ -35,17 +36,30 @@ class TypeScriptHoverProvider implements vscode.HoverProvider { } return new vscode.Hover( - TypeScriptHoverProvider.getContents(response.body), + this.getContents(response.body, response._serverType), typeConverters.Range.fromTextSpan(response.body)); } - private static getContents( - data: Proto.QuickInfoResponseBody + private getContents( + data: Proto.QuickInfoResponseBody, + source: ServerType | undefined, ) { - const parts = []; + const parts: vscode.MarkedString[] = []; if (data.displayString) { - parts.push({ language: 'typescript', value: data.displayString }); + const displayParts: string[] = []; + + if (source === ServerType.Syntax && this.client.capabilities.has(ClientCapability.Semantic)) { + displayParts.push( + localize({ + key: 'loadingPrefix', + comment: ['Prefix displayed for hover entries while the server is still loading'] + }, "(loading...)")); + } + + displayParts.push(data.displayString); + + parts.push({ language: 'typescript', value: displayParts.join(' ') }); } parts.push(markdownDocumentation(data.documentation, data.tags)); return parts; diff --git a/extensions/typescript-language-features/src/lazyClientHost.ts b/extensions/typescript-language-features/src/lazyClientHost.ts index 7f136285215..73dee25e6c3 100644 --- a/extensions/typescript-language-features/src/lazyClientHost.ts +++ b/extensions/typescript-language-features/src/lazyClientHost.ts @@ -11,6 +11,7 @@ import { TsServerProcessFactory } from './tsServer/server'; import { ITypeScriptVersionProvider } from './tsServer/versionProvider'; import TypeScriptServiceClientHost from './typeScriptServiceClientHost'; import { flatten } from './utils/arrays'; +import * as fileSchemes from './utils/fileSchemes'; import { standardLanguageDescriptions } from './utils/languageDescription'; import { lazy, Lazy } from './utils/lazy'; import ManagedFileContextManager from './utils/managedFileContext'; @@ -85,5 +86,6 @@ function isSupportedDocument( supportedLanguage: readonly string[], document: vscode.TextDocument ): boolean { - return supportedLanguage.indexOf(document.languageId) >= 0; + return supportedLanguage.indexOf(document.languageId) >= 0 + && !fileSchemes.disabledSchemes.has(document.uri.scheme); } diff --git a/extensions/typescript-language-features/src/protocol.d.ts b/extensions/typescript-language-features/src/protocol.d.ts index 6e926eb8d7e..e81fe81f2db 100644 --- a/extensions/typescript-language-features/src/protocol.d.ts +++ b/extensions/typescript-language-features/src/protocol.d.ts @@ -1,2 +1,12 @@ import * as Proto from 'typescript/lib/protocol'; export = Proto; + +declare enum ServerType { + Syntax = 'syntax', + Semantic = 'semantic', +} +declare module 'typescript/lib/protocol' { + interface Response { + readonly _serverType?: ServerType; + } +} diff --git a/extensions/typescript-language-features/src/task/taskProvider.ts b/extensions/typescript-language-features/src/task/taskProvider.ts index 0024a3596f3..85d3b574ae3 100644 --- a/extensions/typescript-language-features/src/task/taskProvider.ts +++ b/extensions/typescript-language-features/src/task/taskProvider.ts @@ -203,7 +203,7 @@ class TscTaskProvider implements vscode.TaskProvider { } private getBuildTask(workspaceFolder: vscode.WorkspaceFolder | undefined, label: string, command: string, args: string[], buildTaskidentifier: TypeScriptTaskDefinition): vscode.Task { - const buildTask = new vscode.Task2( + const buildTask = new vscode.Task( buildTaskidentifier, workspaceFolder || vscode.TaskScope.Workspace, localize('buildTscLabel', 'build - {0}', label), diff --git a/extensions/typescript-language-features/src/test/functionCallSnippet.test.ts b/extensions/typescript-language-features/src/test/functionCallSnippet.test.ts index 1289b87b4bc..5c8cf18c73c 100644 --- a/extensions/typescript-language-features/src/test/functionCallSnippet.test.ts +++ b/extensions/typescript-language-features/src/test/functionCallSnippet.test.ts @@ -128,4 +128,13 @@ suite('typescript function call snippets', () => { ).snippet.value, 'foobar(${1:param})$0'); }); + + test('Should skip over this parameter', async () => { + assert.strictEqual( + snippetForFunctionCall( + { label: 'foobar', }, + [{ "text": "function", "kind": "keyword" }, { "text": " ", "kind": "space" }, { "text": "foobar", "kind": "functionName" }, { "text": "(", "kind": "punctuation" }, { "text": "this", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "string", "kind": "keyword" }, { "text": ",", "kind": "punctuation" }, { "text": "param", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "string", "kind": "keyword" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }] + ).snippet.value, + 'foobar(${1:param})$0'); + }); }); diff --git a/extensions/typescript-language-features/src/test/server.test.ts b/extensions/typescript-language-features/src/test/server.test.ts index 7e27e366b5c..5caad737a48 100644 --- a/extensions/typescript-language-features/src/test/server.test.ts +++ b/extensions/typescript-language-features/src/test/server.test.ts @@ -9,6 +9,7 @@ import * as stream from 'stream'; import type * as Proto from '../protocol'; import { NodeRequestCanceller } from '../tsServer/cancellation.electron'; import { ProcessBasedTsServer, TsServerProcess } from '../tsServer/server'; +import { ServerType } from '../typescriptService'; import { nulToken } from '../utils/cancellation'; import { Logger } from '../utils/logger'; import { TelemetryReporter } from '../utils/telemetry'; @@ -64,7 +65,7 @@ suite('Server', () => { test('should send requests with increasing sequence numbers', async () => { const process = new FakeServerProcess(); - const server = new ProcessBasedTsServer('semantic', process, undefined, new NodeRequestCanceller('semantic', tracer), undefined!, NoopTelemetryReporter, tracer); + const server = new ProcessBasedTsServer('semantic', ServerType.Semantic, process, undefined, new NodeRequestCanceller('semantic', tracer), undefined!, NoopTelemetryReporter, tracer); const onWrite1 = process.onWrite(); server.executeImpl('geterr', {}, { isAsync: false, token: nulToken, expectsResult: true }); diff --git a/extensions/typescript-language-features/src/tsServer/server.ts b/extensions/typescript-language-features/src/tsServer/server.ts index fc7841322bd..6ad3d015679 100644 --- a/extensions/typescript-language-features/src/tsServer/server.ts +++ b/extensions/typescript-language-features/src/tsServer/server.ts @@ -9,7 +9,7 @@ import { EventName } from '../protocol.const'; import { CallbackMap } from '../tsServer/callbackMap'; import { RequestItem, RequestQueue, RequestQueueingType } from '../tsServer/requestQueue'; import { TypeScriptServerError } from '../tsServer/serverError'; -import { ServerResponse, TypeScriptRequests } from '../typescriptService'; +import { ServerResponse, ServerType, TypeScriptRequests } from '../typescriptService'; import { TypeScriptServiceConfiguration } from '../utils/configuration'; import { Disposable } from '../utils/dispose'; import { TelemetryReporter } from '../utils/telemetry'; @@ -77,6 +77,7 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe constructor( private readonly _serverId: string, + private readonly _serverSource: ServerType, private readonly _process: TsServerProcess, private readonly _tsServerLogFile: string | undefined, private readonly _requestCanceller: OngoingRequestCanceller, @@ -130,7 +131,14 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe try { switch (message.type) { case 'response': - this.dispatchResponse(message as Proto.Response); + if (this._serverSource) { + this.dispatchResponse({ + ...(message as Proto.Response), + _serverType: this._serverSource + }); + } else { + this.dispatchResponse(message as Proto.Response); + } break; case 'event': diff --git a/extensions/typescript-language-features/src/tsServer/serverError.ts b/extensions/typescript-language-features/src/tsServer/serverError.ts index 42622669498..2653360c9be 100644 --- a/extensions/typescript-language-features/src/tsServer/serverError.ts +++ b/extensions/typescript-language-features/src/tsServer/serverError.ts @@ -39,7 +39,7 @@ export class TypeScriptServerError extends Error { "TypeScriptRequestErrorProperties" : { "command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "serverid" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "sanitizedstack" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "sanitizedstack" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } } */ return { diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts index f9d70d858e0..96b0c6b411e 100644 --- a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts @@ -173,12 +173,19 @@ export class ChildServerProcess extends Disposable implements TsServerProcess { } private static getExecArgv(kind: TsServerProcessKind, configuration: TypeScriptServiceConfiguration): string[] { + const args: string[] = []; + const debugPort = this.getDebugPort(kind); - const inspectFlag = process.env['TSS_DEBUG_BRK'] ? '--inspect-brk' : '--inspect'; - return [ - ...(debugPort ? [`${inspectFlag}=${debugPort}`] : []), - ...(configuration.maxTsServerMemory ? [`--max-old-space-size=${configuration.maxTsServerMemory}`] : []) - ]; + if (debugPort) { + const inspectFlag = ChildServerProcess.getTssDebugBrk() ? '--inspect-brk' : '--inspect'; + args.push(`${inspectFlag}=${debugPort}`); + } + + if (configuration.maxTsServerMemory) { + args.push(`--max-old-space-size=${configuration.maxTsServerMemory}`); + } + + return args; } private static getDebugPort(kind: TsServerProcessKind): number | undefined { @@ -186,7 +193,7 @@ export class ChildServerProcess extends Disposable implements TsServerProcess { // We typically only want to debug the main semantic server return undefined; } - const value = process.env['TSS_DEBUG_BRK'] || process.env['TSS_DEBUG']; + const value = ChildServerProcess.getTssDebugBrk() || ChildServerProcess.getTssDebug(); if (value) { const port = parseInt(value); if (!isNaN(port)) { @@ -196,6 +203,14 @@ export class ChildServerProcess extends Disposable implements TsServerProcess { return undefined; } + private static getTssDebug(): string | undefined { + return process.env[vscode.env.remoteName ? 'TSS_REMOTE_DEBUG' : 'TSS_DEBUG']; + } + + private static getTssDebugBrk(): string | undefined { + return process.env[vscode.env.remoteName ? 'TSS_REMOTE_DEBUG_BRK' : 'TSS_DEBUG_BRK']; + } + private constructor( private readonly _process: child_process.ChildProcess, ) { diff --git a/extensions/typescript-language-features/src/tsServer/spawner.ts b/extensions/typescript-language-features/src/tsServer/spawner.ts index 092ad4f0fb6..7f4aaeefe13 100644 --- a/extensions/typescript-language-features/src/tsServer/spawner.ts +++ b/extensions/typescript-language-features/src/tsServer/spawner.ts @@ -6,7 +6,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { OngoingRequestCancellerFactory } from '../tsServer/cancellation'; -import { ClientCapabilities, ClientCapability } from '../typescriptService'; +import { ClientCapabilities, ClientCapability, ServerType } from '../typescriptService'; import API from '../utils/api'; import { SeparateSyntaxServerConfiguration, TsServerLogLevel, TypeScriptServiceConfiguration } from '../utils/configuration'; import { Logger } from '../utils/logger'; @@ -18,7 +18,6 @@ import { ILogDirectoryProvider } from './logDirectoryProvider'; import { GetErrRoutingTsServer, ITypeScriptServer, ProcessBasedTsServer, SyntaxRoutingTsServer, TsServerDelegate, TsServerProcessFactory, TsServerProcessKind } from './server'; import { TypeScriptVersionManager } from './versionManager'; import { ITypeScriptVersionProvider, TypeScriptVersion } from './versionProvider'; -import * as semver from 'semver'; const enum CompositeServerType { /** Run a single server that handles all commands */ @@ -144,6 +143,7 @@ export class TypeScriptServerSpawner { return new ProcessBasedTsServer( kind, + this.kindToServerType(kind), process!, tsServerLogFile, canceller, @@ -152,6 +152,19 @@ export class TypeScriptServerSpawner { this._tracer); } + private kindToServerType(kind: TsServerProcessKind): ServerType { + switch (kind) { + case TsServerProcessKind.Syntax: + return ServerType.Syntax; + + case TsServerProcessKind.Main: + case TsServerProcessKind.Semantic: + case TsServerProcessKind.Diagnostics: + default: + return ServerType.Semantic; + } + } + private getTsServerArgs( kind: TsServerProcessKind, configuration: TypeScriptServiceConfiguration, @@ -164,11 +177,9 @@ export class TypeScriptServerSpawner { let tsServerLogFile: string | undefined; if (kind === TsServerProcessKind.Syntax) { - if (semver.gte(API.v400rc.fullVersionString, apiVersion.fullVersionString)) { - args.push('--serverMode'); - args.push('partialSemantic'); - } - else { + if (apiVersion.gte(API.v401)) { + args.push('--serverMode', 'partialSemantic'); + } else { args.push('--syntaxOnly'); } } diff --git a/extensions/typescript-language-features/src/typescriptService.ts b/extensions/typescript-language-features/src/typescriptService.ts index de32927d816..8a59d97105c 100644 --- a/extensions/typescript-language-features/src/typescriptService.ts +++ b/extensions/typescript-language-features/src/typescriptService.ts @@ -13,6 +13,11 @@ import { TypeScriptServiceConfiguration } from './utils/configuration'; import { PluginManager } from './utils/plugins'; import { TelemetryReporter } from './utils/telemetry'; +export enum ServerType { + Syntax = 'syntax', + Semantic = 'semantic', +} + export namespace ServerResponse { export class Cancelled { diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 69a3c049040..a9e6bf10d7f 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -532,7 +532,6 @@ export default class TypeScriptServiceClient extends Disposable implements IType preferences: { providePrefixAndSuffixTextForRename: true, allowRenameOfImportPath: true, - // @ts-expect-error, remove after 4.0 protocol update includePackageJsonAutoImports: this._configuration.includePackageJsonAutoImports, }, watchOptions @@ -637,6 +636,10 @@ export default class TypeScriptServiceClient extends Disposable implements IType } public normalizedPath(resource: vscode.Uri): string | undefined { + if (fileSchemes.disabledSchemes.has(resource.scheme)) { + return undefined; + } + switch (resource.scheme) { case fileSchemes.file: { @@ -649,10 +652,6 @@ export default class TypeScriptServiceClient extends Disposable implements IType // Both \ and / must be escaped in regular expressions return result.replace(new RegExp('\\' + this.pathSeparator, 'g'), '/'); } - case fileSchemes.git: - { - return undefined; - } default: { return this.inMemoryResourcePrefix + resource.toString(true); @@ -666,7 +665,9 @@ export default class TypeScriptServiceClient extends Disposable implements IType public toOpenedFilePath(document: vscode.TextDocument): string | undefined { if (!this.bufferSyncSupport.ensureHasBuffer(document.uri)) { - console.error(`Unexpected resource ${document.uri}`); + if (!fileSchemes.disabledSchemes.has(document.uri.scheme)) { + console.error(`Unexpected resource ${document.uri}`); + } return undefined; } return this.toPath(document.uri) || undefined; diff --git a/extensions/typescript-language-features/src/utils/api.ts b/extensions/typescript-language-features/src/utils/api.ts index f797e578121..289c091f5b1 100644 --- a/extensions/typescript-language-features/src/utils/api.ts +++ b/extensions/typescript-language-features/src/utils/api.ts @@ -34,8 +34,8 @@ export default class API { public static readonly v380 = API.fromSimpleString('3.8.0'); public static readonly v381 = API.fromSimpleString('3.8.1'); public static readonly v390 = API.fromSimpleString('3.9.0'); - public static readonly v400rc = API.fromSimpleString('4.0.0-rc'); public static readonly v400 = API.fromSimpleString('4.0.0'); + public static readonly v401 = API.fromSimpleString('4.0.1'); public static fromVersionString(versionString: string): API { let version = semver.valid(versionString); diff --git a/extensions/typescript-language-features/src/utils/configuration.ts b/extensions/typescript-language-features/src/utils/configuration.ts index cde1e14b9a0..f549ff6f9e2 100644 --- a/extensions/typescript-language-features/src/utils/configuration.ts +++ b/extensions/typescript-language-features/src/utils/configuration.ts @@ -66,7 +66,7 @@ export class TypeScriptServiceConfiguration { public readonly maxTsServerMemory: number; public readonly enablePromptUseWorkspaceTsdk: boolean; public readonly watchOptions: protocol.WatchOptions | undefined; - public readonly includePackageJsonAutoImports: string | undefined; + public readonly includePackageJsonAutoImports: 'auto' | 'on' | 'off' | undefined; public static loadFromWorkspace(): TypeScriptServiceConfiguration { return new TypeScriptServiceConfiguration(); @@ -181,8 +181,8 @@ export class TypeScriptServiceConfiguration { return configuration.get('typescript.tsserver.watchOptions'); } - private static readIncludePackageJsonAutoImports(configuration: vscode.WorkspaceConfiguration): string | undefined { - return configuration.get('typescript.preferences.includePackageJsonAutoImports'); + private static readIncludePackageJsonAutoImports(configuration: vscode.WorkspaceConfiguration): 'auto' | 'on' | 'off' | undefined { + return configuration.get<'auto' | 'on' | 'off'>('typescript.preferences.includePackageJsonAutoImports'); } private static readMaxTsServerMemory(configuration: vscode.WorkspaceConfiguration): number { diff --git a/extensions/typescript-language-features/src/utils/fileSchemes.ts b/extensions/typescript-language-features/src/utils/fileSchemes.ts index 4e94d547bd6..d465a60326e 100644 --- a/extensions/typescript-language-features/src/utils/fileSchemes.ts +++ b/extensions/typescript-language-features/src/utils/fileSchemes.ts @@ -6,9 +6,19 @@ export const file = 'file'; export const untitled = 'untitled'; export const git = 'git'; +/** Live share scheme */ +export const vsls = 'vsls'; export const walkThroughSnippet = 'walkThroughSnippet'; export const semanticSupportedSchemes = [ file, untitled, ]; + +/** + * File scheme for which JS/TS language feature should be disabled + */ +export const disabledSchemes = new Set([ + git, + vsls +]); diff --git a/extensions/typescript-language-features/src/utils/snippetForFunctionCall.ts b/extensions/typescript-language-features/src/utils/snippetForFunctionCall.ts index 6faf19eff48..e0185db581b 100644 --- a/extensions/typescript-language-features/src/utils/snippetForFunctionCall.ts +++ b/extensions/typescript-language-features/src/utils/snippetForFunctionCall.ts @@ -73,7 +73,9 @@ function getParameterListParts( const next = displayParts[i + 1]; // Skip optional parameters const nameIsFollowedByOptionalIndicator = next && next.text === '?'; - if (!nameIsFollowedByOptionalIndicator) { + // Skip this parameter + const nameIsThis = part.text === 'this'; + if (!nameIsFollowedByOptionalIndicator && !nameIsThis) { parts.push(part); } hasOptionalParameters = hasOptionalParameters || nameIsFollowedByOptionalIndicator; diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/index.ts b/extensions/vscode-api-tests/src/singlefolder-tests/index.ts index 8379f9b1ab7..ea50f38b1b9 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/index.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/index.ts @@ -6,21 +6,31 @@ const path = require('path'); const testRunner = require('vscode/lib/testrunner'); -const suite = 'Integration Single Folder Tests'; - const options: any = { ui: 'tdd', useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), timeout: 60000 }; +// These integration tests is being run in multiple environments (electron, web, remote) +// so we need to set the suite name based on the environment as the suite name is used +// for the test results file name +let suite = ''; +if (process.env.VSCODE_BROWSER) { + suite = `${process.env.VSCODE_BROWSER} Browser Integration Single Folder Tests`; +} else if (process.env.REMOTE_VSCODE) { + suite = 'Remote Integration Single Folder Tests'; +} else { + suite = 'Integration Single Folder Tests'; +} + if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } 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 8b2f8c057e0..8e2afa3ba13 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { window, Pseudoterminal, EventEmitter, TerminalDimensions, workspace, ConfigurationTarget, Disposable, UIKind, env, EnvironmentVariableMutatorType, EnvironmentVariableMutator, extensions, ExtensionContext } from 'vscode'; +import { window, Pseudoterminal, EventEmitter, TerminalDimensions, workspace, ConfigurationTarget, Disposable, UIKind, env, EnvironmentVariableMutatorType, EnvironmentVariableMutator, extensions, ExtensionContext, TerminalOptions, ExtensionTerminalOptions } from 'vscode'; import { doesNotThrow, equal, ok, deepEqual, throws } from 'assert'; // Disable terminal tests: @@ -168,8 +168,10 @@ import { doesNotThrow, equal, ok, deepEqual, throws } from 'assert'; const terminal = window.createTerminal(options); try { equal(terminal.name, 'foo'); - deepEqual(terminal.creationOptions, options); - throws(() => (terminal.creationOptions).name = 'bad', 'creationOptions should be readonly at runtime'); + const terminalOptions = terminal.creationOptions as TerminalOptions; + equal(terminalOptions.name, 'foo'); + equal(terminalOptions.hideFromUser, true); + throws(() => terminalOptions.name = 'bad', 'creationOptions should be readonly at runtime'); } catch (e) { done(e); return; @@ -460,10 +462,6 @@ import { doesNotThrow, equal, ok, deepEqual, throws } from 'assert'; } term.show(); disposables.push(window.onDidChangeTerminalDimensions(e => { - if (e.dimensions.columns === 0 || e.dimensions.rows === 0) { - // HACK: Ignore the event if dimension(s) are zero (#83778) - return; - } // The default pty dimensions have a chance to appear here since override // dimensions happens after the terminal is created. If so just ignore and // wait for the right dimensions @@ -609,8 +607,10 @@ import { doesNotThrow, equal, ok, deepEqual, throws } from 'assert'; const terminal = window.createTerminal(options); try { equal(terminal.name, 'foo'); - deepEqual(terminal.creationOptions, options); - throws(() => (terminal.creationOptions).name = 'bad', 'creationOptions should be readonly at runtime'); + const terminalOptions = terminal.creationOptions as ExtensionTerminalOptions; + equal(terminalOptions.name, 'foo'); + equal(terminalOptions.pty, pty); + throws(() => terminalOptions.name = 'bad', 'creationOptions should be readonly at runtime'); } catch (e) { done(e); } diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts index 8c8d5e64ca7..9187b34e548 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts @@ -429,6 +429,8 @@ suite('vscode API - window', () => { }); test('showQuickPick, select first two', async function () { + const label = 'showQuickPick, select first two'; + let i = 0; const resolves: ((value: string) => void)[] = []; let done: () => void; const unexpected = new Promise((resolve, reject) => { @@ -440,16 +442,26 @@ suite('vscode API - window', () => { canPickMany: true }); const first = new Promise(resolve => resolves.push(resolve)); - await new Promise(resolve => setTimeout(resolve, 10)); // Allow UI to update. + console.log(`${label}: ${++i}`); + await new Promise(resolve => setTimeout(resolve, 100)); // Allow UI to update. + console.log(`${label}: ${++i}`); await commands.executeCommand('workbench.action.quickOpenSelectNext'); + console.log(`${label}: ${++i}`); assert.equal(await first, 'eins'); + console.log(`${label}: ${++i}`); await commands.executeCommand('workbench.action.quickPickManyToggle'); + console.log(`${label}: ${++i}`); const second = new Promise(resolve => resolves.push(resolve)); await commands.executeCommand('workbench.action.quickOpenSelectNext'); + console.log(`${label}: ${++i}`); assert.equal(await second, 'zwei'); + console.log(`${label}: ${++i}`); await commands.executeCommand('workbench.action.quickPickManyToggle'); + console.log(`${label}: ${++i}`); await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); + console.log(`${label}: ${++i}`); assert.deepStrictEqual(await picks, ['eins', 'zwei']); + console.log(`${label}: ${++i}`); done!(); return unexpected; }); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts index 53acdcf23dc..37426834335 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { window, tasks, Disposable, TaskDefinition, Task, EventEmitter, CustomExecution, Pseudoterminal, TaskScope, commands, Task2, env, UIKind, ShellExecution, TaskExecution, Terminal, Event } from 'vscode'; +import { window, tasks, Disposable, TaskDefinition, Task, EventEmitter, CustomExecution, Pseudoterminal, TaskScope, commands, env, UIKind, ShellExecution, TaskExecution, Terminal, Event } from 'vscode'; // Disable tasks tests: // - Web https://github.com/microsoft/vscode/issues/90528 @@ -94,7 +94,7 @@ import { window, tasks, Disposable, TaskDefinition, Task, EventEmitter, CustomEx }; return Promise.resolve(pty); }); - const task = new Task2(kind, TaskScope.Workspace, taskName, taskType, execution); + const task = new Task(kind, TaskScope.Workspace, taskName, taskType, execution); result.push(task); return result; }, @@ -151,7 +151,7 @@ import { window, tasks, Disposable, TaskDefinition, Task, EventEmitter, CustomEx }; return Promise.resolve(pty); }); - const task = new Task2(kind, TaskScope.Workspace, taskName, taskType, execution); + const task = new Task(kind, TaskScope.Workspace, taskName, taskType, execution); result.push(task); return result; }, diff --git a/extensions/vscode-api-tests/src/workspace-tests/index.ts b/extensions/vscode-api-tests/src/workspace-tests/index.ts index dfef493b2ab..9486d8ed3e5 100644 --- a/extensions/vscode-api-tests/src/workspace-tests/index.ts +++ b/extensions/vscode-api-tests/src/workspace-tests/index.ts @@ -6,21 +6,31 @@ const path = require('path'); const testRunner = require('vscode/lib/testrunner'); -const suite = 'Integration Workspace Tests'; - const options: any = { ui: 'tdd', useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), timeout: 60000 }; +// These integration tests is being run in multiple environments (electron, web, remote) +// so we need to set the suite name based on the environment as the suite name is used +// for the test results file name +let suite = ''; +if (process.env.VSCODE_BROWSER) { + suite = `${process.env.VSCODE_BROWSER} Browser Integration Workspace Tests`; +} else if (process.env.REMOTE_VSCODE) { + suite = 'Remote Integration Workspace Tests'; +} else { + suite = 'Integration Workspace Tests'; +} + if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/vscode-colorize-tests/src/index.ts b/extensions/vscode-colorize-tests/src/index.ts index a315ee36112..691ba5c6f07 100644 --- a/extensions/vscode-colorize-tests/src/index.ts +++ b/extensions/vscode-colorize-tests/src/index.ts @@ -20,7 +20,7 @@ if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/vscode-custom-editor-tests/src/test/index.ts b/extensions/vscode-custom-editor-tests/src/test/index.ts index 6d80cca8048..a60622b2f28 100644 --- a/extensions/vscode-custom-editor-tests/src/test/index.ts +++ b/extensions/vscode-custom-editor-tests/src/test/index.ts @@ -20,7 +20,7 @@ if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/vscode-notebook-tests/package.json b/extensions/vscode-notebook-tests/package.json index 4ed7605f894..4454b25fb2a 100644 --- a/extensions/vscode-notebook-tests/package.json +++ b/extensions/vscode-notebook-tests/package.json @@ -62,8 +62,9 @@ ], "notebookOutputRenderer": [ { - "viewType": "notebookCoreTestRenderer", + "id": "notebookCoreTestRenderer", "displayName": "Notebook Core Test Renderer", + "entrypoint": "./src/customRenderer.js", "mimeTypes": [ "text/custom" ] @@ -77,6 +78,12 @@ "group": "inline@1" } ] - } + }, + "jsonValidation": [ + { + "fileMatch": "vscode://vscode-notebook-cell-metadata/*", + "url": "vscode://schemas/notebook/cellmetadata" + } + ] } } diff --git a/extensions/vscode-notebook-tests/src/customRenderer.js b/extensions/vscode-notebook-tests/src/customRenderer.js index 75e2ec1eb7a..f23538e38a7 100644 --- a/extensions/vscode-notebook-tests/src/customRenderer.js +++ b/extensions/vscode-notebook-tests/src/customRenderer.js @@ -11,3 +11,11 @@ vscode.postMessage({ firstMessage: true } }); + +const notebook = acquireNotebookRendererApi('notebookCoreTestRenderer'); + +notebook.onDidCreateOutput(({ element, mimeType }) => { + const div = document.createElement('div'); + div.innerText = `Hello ${mimeType}!`; + element.appendChild(div); +}); diff --git a/extensions/vscode-notebook-tests/src/index.ts b/extensions/vscode-notebook-tests/src/index.ts index 2125e68c3d8..293c02db743 100644 --- a/extensions/vscode-notebook-tests/src/index.ts +++ b/extensions/vscode-notebook-tests/src/index.ts @@ -6,21 +6,31 @@ const path = require('path'); const testRunner = require('vscode/lib/testrunner'); -const suite = 'Integration Notebook Tests'; - const options: any = { ui: 'tdd', useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), timeout: 60000 }; +// These integration tests is being run in multiple environments (electron, web, remote) +// so we need to set the suite name based on the environment as the suite name is used +// for the test results file name +let suite = ''; +if (process.env.VSCODE_BROWSER) { + suite = `${process.env.VSCODE_BROWSER} Browser Integration Notebook Tests`; +} else if (process.env.REMOTE_VSCODE) { + suite = 'Remote Integration Notebook Tests'; +} else { + suite = 'Integration Notebook Tests'; +} + if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/vscode-notebook-tests/src/notebook.test.ts b/extensions/vscode-notebook-tests/src/notebook.test.ts index 378008c4d82..d549dc8dc3f 100644 --- a/extensions/vscode-notebook-tests/src/notebook.test.ts +++ b/extensions/vscode-notebook-tests/src/notebook.test.ts @@ -70,8 +70,11 @@ async function saveFileAndCloseAll(resource: vscode.Uri) { await documentClosed; } -async function saveAllFilesAndCloseAll(resource: vscode.Uri) { +async function saveAllFilesAndCloseAll(resource: vscode.Uri | undefined) { const documentClosed = new Promise((resolve, _reject) => { + if (!resource) { + return resolve(); + } const d = vscode.notebook.onDidCloseNotebookDocument(e => { if (e.uri.toString() === resource.toString()) { d.dispose(); @@ -131,6 +134,46 @@ suite('Notebook API tests', () => { await firstDocumentClose; }); + test('notebook open/close, all cell-documents are ready', async function () { + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + + const p = getEventOncePromise(vscode.notebook.onDidOpenNotebookDocument).then(notebook => { + for (let cell of notebook.cells) { + const doc = vscode.workspace.textDocuments.find(doc => doc.uri.toString() === cell.uri.toString()); + assert.ok(doc); + assert.strictEqual(doc === cell.document, true); + assert.strictEqual(doc?.languageId, cell.language); + assert.strictEqual(doc?.isDirty, false); + assert.strictEqual(doc?.isClosed, false); + } + }); + + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + await p; + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + test('notebook open/close, notebook ready when cell-document open event is fired', async function () { + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + let didHappen = false; + const p = getEventOncePromise(vscode.workspace.onDidOpenTextDocument).then(doc => { + if (doc.uri.scheme !== 'vscode-notebook-cell') { + return; + } + const notebook = vscode.notebook.notebookDocuments.find(notebook => { + const cell = notebook.cells.find(cell => cell.document === doc); + return Boolean(cell); + }); + assert.ok(notebook, `notebook for cell ${doc.uri} NOT found`); + didHappen = true; + }); + + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + await p; + assert.strictEqual(didHappen, true); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + test('shared document in notebook editors', async function () { assertInitalState(); @@ -353,23 +396,102 @@ suite('Notebook API tests', () => { await saveFileAndCloseAll(resource); }); - test('edit API', async function () { + test('edit API (replaceCells)', async function () { assertInitalState(); const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); const cellsChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); await vscode.notebook.activeNotebookEditor!.edit(editBuilder => { - editBuilder.insert(1, 'test 2', 'javascript', vscode.CellKind.Code, [], undefined); + editBuilder.replaceCells(1, 0, [{ cellKind: vscode.CellKind.Code, language: 'javascript', source: 'test 2', outputs: [], metadata: undefined }]); }); const cellChangeEventRet = await cellsChangeEvent; - assert.equal(cellChangeEventRet.document, vscode.notebook.activeNotebookEditor?.document); - assert.equal(cellChangeEventRet.changes.length, 1); - assert.deepEqual(cellChangeEventRet.changes[0].start, 1); - assert.deepEqual(cellChangeEventRet.changes[0].deletedCount, 0); - assert.equal(cellChangeEventRet.changes[0].items[0], vscode.notebook.activeNotebookEditor!.document.cells[1]); + assert.strictEqual(cellChangeEventRet.document === vscode.notebook.activeNotebookEditor?.document, true); + assert.strictEqual(cellChangeEventRet.document.isDirty, true); + assert.strictEqual(cellChangeEventRet.changes.length, 1); + assert.strictEqual(cellChangeEventRet.changes[0].start, 1); + assert.strictEqual(cellChangeEventRet.changes[0].deletedCount, 0); + assert.strictEqual(cellChangeEventRet.changes[0].items[0] === vscode.notebook.activeNotebookEditor!.document.cells[1], true); + await saveAllFilesAndCloseAll(resource); + }); + + test('edit API (replaceOutput)', async function () { + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + + await vscode.notebook.activeNotebookEditor!.edit(editBuilder => { + editBuilder.replaceOutput(0, [{ outputKind: vscode.CellOutputKind.Rich, data: { foo: 'bar' } }]); + }); + + const document = vscode.notebook.activeNotebookEditor?.document!; + assert.strictEqual(document.isDirty, true); + assert.strictEqual(document.cells.length, 1); + assert.strictEqual(document.cells[0].outputs.length, 1); + assert.strictEqual(document.cells[0].outputs[0].outputKind, vscode.CellOutputKind.Rich); + + await saveAllFilesAndCloseAll(undefined); + }); + + test('edit API (replaceOutput, event)', async function () { + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + + const outputChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeCellOutputs); + await vscode.notebook.activeNotebookEditor!.edit(editBuilder => { + editBuilder.replaceOutput(0, [{ outputKind: vscode.CellOutputKind.Rich, data: { foo: 'bar' } }]); + }); + + const value = await outputChangeEvent; + assert.strictEqual(value.document === vscode.notebook.activeNotebookEditor?.document, true); + assert.strictEqual(value.document.isDirty, true); + assert.strictEqual(value.cells.length, 1); + assert.strictEqual(value.cells[0].outputs.length, 1); + assert.strictEqual(value.cells[0].outputs[0].outputKind, vscode.CellOutputKind.Rich); + + await saveAllFilesAndCloseAll(undefined); + }); + + test('edit API (replaceMetadata)', async function () { + + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + + await vscode.notebook.activeNotebookEditor!.edit(editBuilder => { + editBuilder.replaceMetadata(0, { inputCollapsed: true, executionOrder: 17 }); + }); + + const document = vscode.notebook.activeNotebookEditor?.document!; + assert.strictEqual(document.cells.length, 1); + assert.strictEqual(document.cells[0].metadata.executionOrder, 17); + assert.strictEqual(document.cells[0].metadata.inputCollapsed, true); + + assert.strictEqual(document.isDirty, true); + await saveFileAndCloseAll(resource); + }); + + test('edit API (replaceMetadata, event)', async function () { + + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + + const event = getEventOncePromise(vscode.notebook.onDidChangeCellMetadata); + + await vscode.notebook.activeNotebookEditor!.edit(editBuilder => { + editBuilder.replaceMetadata(0, { inputCollapsed: true, executionOrder: 17 }); + }); + + const data = await event; + assert.strictEqual(data.document, vscode.notebook.activeNotebookEditor?.document); + assert.strictEqual(data.cell.metadata.executionOrder, 17); + assert.strictEqual(data.cell.metadata.inputCollapsed, true); + + assert.strictEqual(data.document.isDirty, true); await saveFileAndCloseAll(resource); }); diff --git a/extensions/vscode-notebook-tests/src/notebookSmokeTestMain.ts b/extensions/vscode-notebook-tests/src/notebookSmokeTestMain.ts index 0cec3fe06d6..10ab61306dc 100644 --- a/extensions/vscode-notebook-tests/src/notebookSmokeTestMain.ts +++ b/extensions/vscode-notebook-tests/src/notebookSmokeTestMain.ts @@ -67,8 +67,9 @@ export function smokeTestActivate(context: vscode.ExtensionContext): any { } })); - context.subscriptions.push(vscode.notebook.registerNotebookKernel('notebookSmokeTest', ['*.smoke-nb'], { + const kernel: vscode.NotebookKernel = { label: 'notebookSmokeTest', + isPreferred: true, executeAllCells: async (_document: vscode.NotebookDocument) => { for (let i = 0; i < _document.cells.length; i++) { _document.cells[i].outputs = [{ @@ -94,6 +95,12 @@ export function smokeTestActivate(context: vscode.ExtensionContext): any { return; }, cancelCellExecution: async () => { } + }; + + context.subscriptions.push(vscode.notebook.registerNotebookKernelProvider({ filenamePattern: '*.smoke-nb' }, { + provideKernels: async () => { + return [kernel]; + } })); context.subscriptions.push(vscode.commands.registerCommand('vscode-notebook-tests.debugAction', async (cell: vscode.NotebookCell) => { diff --git a/extensions/vscode-notebook-tests/src/notebookTestMain.ts b/extensions/vscode-notebook-tests/src/notebookTestMain.ts index c8ea3282c2c..03c8c7435ce 100644 --- a/extensions/vscode-notebook-tests/src/notebookTestMain.ts +++ b/extensions/vscode-notebook-tests/src/notebookTestMain.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import * as path from 'path'; import { smokeTestActivate } from './notebookSmokeTestMain'; export function activate(context: vscode.ExtensionContext): any { @@ -60,8 +59,9 @@ export function activate(context: vscode.ExtensionContext): any { } })); - context.subscriptions.push(vscode.notebook.registerNotebookKernel('notebookKernelTest', ['*.vsctestnb'], { + const kernel: vscode.NotebookKernel = { label: 'Notebook Test Kernel', + isPreferred: true, executeAllCells: async (_document: vscode.NotebookDocument) => { const cell = _document.cells[0]; @@ -116,17 +116,11 @@ export function activate(context: vscode.ExtensionContext): any { return; }, cancelCellExecution: async (_document: vscode.NotebookDocument, _cell: vscode.NotebookCell) => { } - })); + }; - const preloadUri = vscode.Uri.file(path.resolve(__dirname, '../src/customRenderer.js')); - context.subscriptions.push(vscode.notebook.registerNotebookOutputRenderer('notebookCoreTestRenderer', { - mimeTypes: [ - 'text/custom' - ] - }, { - preloads: [preloadUri], - render(_document: vscode.NotebookDocument, _request: vscode.NotebookRenderRequest): string { - return '
test
'; + context.subscriptions.push(vscode.notebook.registerNotebookKernelProvider({ filenamePattern: '*.vsctestnb' }, { + provideKernels: async () => { + return [kernel]; } })); } diff --git a/extensions/vscode-web-playground/.gitignore b/extensions/vscode-web-playground/.gitignore deleted file mode 100644 index c19bd94aaa7..00000000000 --- a/extensions/vscode-web-playground/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -dist -out -node_modules diff --git a/extensions/vscode-web-playground/.vscode/tasks.json b/extensions/vscode-web-playground/.vscode/tasks.json deleted file mode 100644 index 390a93a3a7f..00000000000 --- a/extensions/vscode-web-playground/.vscode/tasks.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "2.0.0", - "command": "npm", - "type": "shell", - "presentation": { - "reveal": "silent" - }, - "args": ["run", "compile"], - "isBackground": true, - "problemMatcher": "$tsc-watch" -} diff --git a/extensions/vscode-web-playground/.vscodeignore b/extensions/vscode-web-playground/.vscodeignore deleted file mode 100644 index 32fe3f03697..00000000000 --- a/extensions/vscode-web-playground/.vscodeignore +++ /dev/null @@ -1,11 +0,0 @@ -.vscode/** -build/** -dist/** -out/** -src/** -typings/** -.gitignore -extension-browser.webpack.config.js -extension.webpack.config.js -tsconfig.json -yarn.lock diff --git a/extensions/vscode-web-playground/extension-browser.webpack.config.js b/extensions/vscode-web-playground/extension-browser.webpack.config.js deleted file mode 100644 index dfd50aeff96..00000000000 --- a/extensions/vscode-web-playground/extension-browser.webpack.config.js +++ /dev/null @@ -1,18 +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 path = require('path'); -const withBrowserDefaults = require('../shared.webpack.config').browser; - -module.exports = withBrowserDefaults({ - context: __dirname, - node: false, - entry: { - extension: './src/extension.ts', - } -}); diff --git a/extensions/vscode-web-playground/package.json b/extensions/vscode-web-playground/package.json deleted file mode 100644 index 954aec0fae9..00000000000 --- a/extensions/vscode-web-playground/package.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "name": "vscode-web-playground", - "description": "Web playground for VS Code", - "version": "0.0.1", - "publisher": "vscode", - "license": "MIT", - "enableProposedApi": true, - "private": true, - "activationEvents": [ - "onFileSystem:memfs", - "onFileSystem:github", - "onDebug" - ], - "browser": "./dist/browser/extension", - "main": "./out/extension", - "engines": { - "vscode": "^1.25.0" - }, - "contributes": { - "taskDefinitions": [ - { - "type": "custombuildscript", - "required": [ - "flavor" - ], - "properties": { - "flavor": { - "type": "string", - "description": "The build flavor. Should be either '32' or '64'." - }, - "flags": { - "type": "array", - "description": "Additional build flags." - } - } - } - ], - "breakpoints": [ - { - "language": "markdown" - } - ], - "debuggers": [ - { - "type": "mock", - "label": "Mock Debug", - "languages": [ - "markdown" - ], - "configurationAttributes": { - "launch": { - "required": [ - "program" - ], - "properties": { - "program": { - "type": "string", - "description": "Absolute path to a text file.", - "default": "${workspaceFolder}/file.md" - }, - "stopOnEntry": { - "type": "boolean", - "description": "Automatically stop after launch.", - "default": true - }, - "trace": { - "type": "boolean", - "description": "Enable logging of the Debug Adapter Protocol.", - "default": true - } - } - } - }, - "initialConfigurations": [ - { - "type": "mock", - "request": "launch", - "name": "Debug file.md", - "program": "${workspaceFolder}/file.md" - } - ] - } - ], - "resourceLabelFormatters": [ - { - "scheme": "github", - "authority": "*", - "formatting": { - "label": "${authority}${path}", - "separator": "/", - "workspaceSuffix": "GitHub" - } - } - ] - }, - "scripts": { - "compile": "node ./node_modules/vscode/bin/compile -watch -p ./", - "compile-web": "npx webpack-cli --config extension.webpack.config --mode none", - "watch-web": "npx webpack-cli --config extension.webpack.config --mode none --watch --info-verbosity verbose", - "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-web-playground ./tsconfig.json" - }, - "devDependencies": { - "@types/mocha": "2.2.43", - "mocha-junit-reporter": "^1.17.0", - "mocha-multi-reporters": "^1.1.7" - } -} diff --git a/extensions/vscode-web-playground/src/exampleFiles.ts b/extensions/vscode-web-playground/src/exampleFiles.ts deleted file mode 100644 index a385f7b72e7..00000000000 --- a/extensions/vscode-web-playground/src/exampleFiles.ts +++ /dev/null @@ -1,310 +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 const largeTSFile = `/// -/// - -module Mankala { -export var storeHouses = [6,13]; -export var svgNS = 'http://www.w3.org/2000/svg'; - -function createSVGRect(r:Rectangle) { - var rect = document.createElementNS(svgNS,'rect'); - rect.setAttribute('x', r.x.toString()); - rect.setAttribute('y', r.y.toString()); - rect.setAttribute('width', r.width.toString()); - rect.setAttribute('height', r.height.toString()); - return rect; -} - -function createSVGEllipse(r:Rectangle) { - var ell = document.createElementNS(svgNS,'ellipse'); - ell.setAttribute('rx',(r.width/2).toString()); - ell.setAttribute('ry',(r.height/2).toString()); - ell.setAttribute('cx',(r.x+r.width/2).toString()); - ell.setAttribute('cy',(r.y+r.height/2).toString()); - return ell; -} - -function createSVGEllipsePolar(angle:number,radius:number,tx:number,ty:number,cxo:number,cyo:number) { - var ell = document.createElementNS(svgNS,'ellipse'); - ell.setAttribute('rx',radius.toString()); - ell.setAttribute('ry',(radius/3).toString()); - ell.setAttribute('cx',cxo.toString()); - ell.setAttribute('cy',cyo.toString()); - var dangle = angle*(180/Math.PI); - ell.setAttribute('transform','rotate('+dangle+','+cxo+','+cyo+') translate('+tx+','+ty+')'); - return ell; -} - -function createSVGInscribedCircle(sq:Square) { - var circle = document.createElementNS(svgNS,'circle'); - circle.setAttribute('r',(sq.length/2).toString()); - circle.setAttribute('cx',(sq.x+(sq.length/2)).toString()); - circle.setAttribute('cy',(sq.y+(sq.length/2)).toString()); - return circle; -} - -export class Position { - - seedCounts:number[]; - startMove:number; - turn:number; - - constructor(seedCounts:number[],startMove:number,turn:number) { - this.seedCounts = seedCounts; - this.startMove = startMove; - this.turn = turn; - } - - score() { - var baseScore = this.seedCounts[storeHouses[1-this.turn]]-this.seedCounts[storeHouses[this.turn]]; - var otherSpaces = homeSpaces[this.turn]; - var sum = 0; - for (var k = 0,len = otherSpaces.length;k0) { - features.clear(); - var len = this.seedCounts.length; - for (var i = 0;i0) { - if (nextSpace==storeHouses[this.turn]) { - features.seedStoredCount++; - } - if ((nextSpace!=storeHouses[1-this.turn])) { - nextSeedCounts[nextSpace]++; - seedCount--; - } - if (seedCount==0) { - if (nextSpace==storeHouses[this.turn]) { - features.turnContinues = true; - } - else { - if ((nextSeedCounts[nextSpace]==1)&& - (nextSpace>=firstHomeSpace[this.turn])&& - (nextSpace<=lastHomeSpace[this.turn])) { - // capture - var capturedSpace = capturedSpaces[nextSpace]; - if (capturedSpace>=0) { - features.spaceCaptured = capturedSpace; - features.capturedCount = nextSeedCounts[capturedSpace]; - nextSeedCounts[capturedSpace] = 0; - nextSeedCounts[storeHouses[this.turn]] += features.capturedCount; - features.seedStoredCount += nextSeedCounts[capturedSpace]; - } - } - } - } - nextSpace = (nextSpace+1)%14; - } - return true; - } - else { - return false; - } - } -} - -export class SeedCoords { - tx:number; - ty:number; - angle:number; - - constructor(tx:number, ty:number, angle:number) { - this.tx = tx; - this.ty = ty; - this.angle = angle; - } -} - -export class DisplayPosition extends Position { - - config:SeedCoords[][]; - - constructor(seedCounts:number[],startMove:number,turn:number) { - super(seedCounts,startMove,turn); - - this.config = []; - - for (var i = 0;i(); - } - } - - - seedCircleRect(rect:Rectangle,seedCount:number,board:Element,seed:number) { - var coords = this.config[seed]; - var sq = rect.inner(0.95).square(); - var cxo = (sq.width/2)+sq.x; - var cyo = (sq.height/2)+sq.y; - var seedNumbers = [5,7,9,11]; - var ringIndex = 0; - var ringRem = seedNumbers[ringIndex]; - var angleDelta = (2*Math.PI)/ringRem; - var angle = angleDelta; - var seedLength = sq.width/(seedNumbers.length<<1); - var crMax = sq.width/2-(seedLength/2); - var pit = createSVGInscribedCircle(sq); - if (seed<7) { - pit.setAttribute('fill','brown'); - } - else { - pit.setAttribute('fill','saddlebrown'); - } - board.appendChild(pit); - var seedsSeen = 0; - while (seedCount > 0) { - if (ringRem == 0) { - ringIndex++; - ringRem = seedNumbers[ringIndex]; - angleDelta = (2*Math.PI)/ringRem; - angle = angleDelta; - } - var tx:number; - var ty:number; - var tangle = angle; - if (coords.length>seedsSeen) { - tx = coords[seedsSeen].tx; - ty = coords[seedsSeen].ty; - tangle = coords[seedsSeen].angle; - } - else { - tx = (Math.random()*crMax)-(crMax/3); - ty = (Math.random()*crMax)-(crMax/3); - coords[seedsSeen] = new SeedCoords(tx,ty,angle); - } - var ell = createSVGEllipsePolar(tangle,seedLength,tx,ty,cxo,cyo); - board.appendChild(ell); - angle += angleDelta; - ringRem--; - seedCount--; - seedsSeen++; - } - } - - toCircleSVG() { - var seedDivisions = 14; - var board = document.createElementNS(svgNS,'svg'); - var boardRect = new Rectangle(0,0,1800,800); - board.setAttribute('width','1800'); - board.setAttribute('height','800'); - var whole = createSVGRect(boardRect); - whole.setAttribute('fill','tan'); - board.appendChild(whole); - var labPlayLab = boardRect.proportionalSplitVert(20,760,20); - var playSurface = labPlayLab[1]; - var storeMainStore = playSurface.proportionalSplitHoriz(8,48,8); - var mainPair = storeMainStore[1].subDivideVert(2); - var playerRects = [mainPair[0].subDivideHoriz(6), mainPair[1].subDivideHoriz(6)]; - // reverse top layer because storehouse on left - for (var k = 0;k<3;k++) { - var temp = playerRects[0][k]; - playerRects[0][k] = playerRects[0][5-k]; - playerRects[0][5-k] = temp; - } - var storehouses = [storeMainStore[0],storeMainStore[2]]; - var playerSeeds = this.seedCounts.length>>1; - for (var i = 0;i<2;i++) { - var player = playerRects[i]; - var storehouse = storehouses[i]; - var r:Rectangle; - for (var j = 0;j(); - } - } - } - return board; - } -} -} -`; - -export const debuggableFile = `# VS Code Mock Debug - -This is a starter sample for developing VS Code debug adapters. - -**Mock Debug** simulates a debug adapter for Visual Studio Code. -It supports *step*, *continue*, *breakpoints*, *exceptions*, and -*variable access* but it is not connected to any real debugger. - -The sample is meant as an educational piece showing how to implement a debug -adapter for VS Code. It can be used as a starting point for developing a real adapter. - -More information about how to develop a new debug adapter can be found -[here](https://code.visualstudio.com/docs/extensions/example-debuggers). -Or discuss debug adapters on Gitter: -[![Gitter Chat](https://img.shields.io/badge/chat-online-brightgreen.svg)](https://gitter.im/Microsoft/vscode) - -## Using Mock Debug - -* Install the **Mock Debug** extension in VS Code. -* Create a new 'program' file 'readme.md' and enter several lines of arbitrary text. -* Switch to the debug viewlet and press the gear dropdown. -* Select the debug environment "Mock Debug". -* Press the green 'play' button to start debugging. - -You can now 'step through' the 'readme.md' file, set and hit breakpoints, and run into exceptions (if the word exception appears in a line). - -![Mock Debug](file.jpg) - -## Build and Run - -[![build status](https://travis-ci.org/Microsoft/vscode-mock-debug.svg?branch=master)](https://travis-ci.org/Microsoft/vscode-mock-debug) -[![build status](https://ci.appveyor.com/api/projects/status/empmw5q1tk6h1fly/branch/master?svg=true)](https://ci.appveyor.com/project/weinand/vscode-mock-debug) - - -* Clone the project [https://github.com/Microsoft/vscode-mock-debug.git](https://github.com/Microsoft/vscode-mock-debug.git) -* Open the project folder in VS Code. -* Press 'F5' to build and launch Mock Debug in another VS Code window. In that window: -* Open a new workspace, create a new 'program' file 'readme.md' and enter several lines of arbitrary text. -* Switch to the debug viewlet and press the gear dropdown. -* Select the debug environment "Mock Debug". -* Press 'F5' to start debugging.`; - -export function getImageFile(): Uint8Array { - const data = atob(`/9j/4AAQSkZJRgABAQAASABIAAD/2wCEAA4ODg4ODhcODhchFxcXIS0hISEhLTktLS0tLTlFOTk5OTk5RUVFRUVFRUVSUlJSUlJgYGBgYGxsbGxsbGxsbGwBERISGxkbLxkZL3FMP0xxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcf/AABEIAFYAZAMBIgACEQEDEQH/xAB1AAACAwEBAQAAAAAAAAAAAAAABAMFBgIBBxAAAgIBAwMCBQQCAwAAAAAAAQIAAxEEBSESMUFRcRMiIzJhFIGRoQbBQlKxAQEBAQEAAAAAAAAAAAAAAAABAgADEQEBAQADAQEAAAAAAAAAAAAAARESITECQf/aAAwDAQACEQMRAD8A2LEZkLc/bKxbdYEHWoyfEze56zXpqRTTYUyPHiVrY2TVZyMzhFZMg8iYE6jcVXAusY98KMnj2lhRu+4aLoGuTNTYPV5APnyDNyPFp6EY3EsO3kxnVVLZVg8z2tw9YsXkGQpcbGIbxHQzep0vw8Jgc8n28CJJRY30lBwzf1iaa2ku/HmMV01VW/k/6hh0abTDTafpPcTytmckEewjeosAqJEj0yDo6yO/rFLzoGME5nIAXtGSM9uwnjLn8zFECw7QneITMWouR7gj9/Ep94061bjXa32WDGfzOGuCXKy9/wDc0FlFe5aX4OpHJHBHcSfT4w246bWJar6MsCwKnp9DOF0r6XRiu5snvg9hNK217vQeih0tXwzcED895R7voNfWoN9gOT2QH/2T3mHrda3Y+p9ppZuSV/qR0j6r+5ju2oun2ypOwCAASGikISzdySf5lxLsAdRPpIqw91xC/wDHvGbAAh88RnSVCjT9b8E/MYsguerTqWuYKo8k4ESTcttsPSmoQ+zCZPWPbvWqsvLE0IxCL4wPP7xEW7TXeKsvaGABOMdLef2ky7ejevX0tBWy5Qhh6jmS9IIxPm6XazbW69K56M/aeRibnSaqyytWtGCfE0+tazDhrHpCdixT5EJSWD1BPkcjsYxpN21FWEcdu0dG3hl8rIX0YqUgDqkSrq/0+6oyfOOZT7hqxqLMKMk8ARfS0fqGatAR04yCY+u3OpLt38e0rQl0tzsFrc8rxj0lqqDHMzujIXUMGPI4mjS1MTCvG8gRLddYE2811n5nHTJ9RaAsztzZ1AZhlX9fBi0VWgWzbSqahfpWfa/iSnatMuqOpVgVPIHGMzc6erS3aQVOoZSMFTK19i2pTwGA9Axx/E58b+K2M8lP6/Urp6BkA5Y+OPE112nrIFeOw8RMajQ7dWU0iAH8TyrVG0mw8EypMFuk7K9TS5RGJHiEYsuUtmEWO1KO2RGDRSVJzj1MiQhOQIx8QEYK5hGpUUJVc1lTgcDjEe1FPxqGQHBZSMiQqa8/Z38xgOoHB/aIfJNVZrdFqirsVbsfzLXT7+UQLYmcDHBlh/k+g+KP1dOCV+4efcTNbdtGq3CxQiMKyeX7CGqxqtDuK7lYK2BXnAz3JMuNZoPpDAyV5zHNt2bRbcA1S/Pjljyf7jerWxx0V4wQeZgynxrUXoUnIif629GJY595cptr1N9XJYjOfEi1G3LYMLgH1m04qxelrAtnj/qZYIvUPpMcHwYtTT8FzVaMN6+sslqVF6gcQ1sRivPccwjS314+bGYRBnqzws6FhUfL7CQ8gdI7+TDIHHgcSVGBYRznMXfUL2J5ngPUOYCpfM2tiq1tnUpVRnMe0DGtAKyQIw+mU4GJCKmrPy+I6V0lxYYIzxOCtdjZyVIMRqtPsYx8RT37+sdRhsFlHzcyC0J0kmcfqFX5cxC7VAk4OPUQtM+UVtYf7vH8iKP8SnKg5U9xHQwsGV7jxF9QnWACMEcgwlUjT4ZUE+YRRLGRehwciEpLRMAAT6SALlIQkF4kl7HEIQLwuQfac9RPeEJi5H3TruvvmEJo1QOcgGQuvVg+sITM8rDKeDHVItXkQhKgqM6esnJEIQlJf//Z`); - return Uint8Array.from([...data].map(x => x.charCodeAt(0))); -} - -// encoded from 'ŠŠ‘Š’Š“Š”Š•Š–Š—Š˜Š™ŠšŠ›ŠœŠŠžŠŸŠ Š”Š¢Š£Š¤Š„Š¦Š§ŠØŠ©ŠŖŠ«Š¬Š­Š®ŠÆŠ°Š±Š²Š³Š“ŠµŠ¶Š·ŠøŠ¹ŠŗŠ»Š¼Š½Š¾Šæрстуфхцчшщъыьэюя' -export const windows1251File = Uint8Array.from([192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255]); - -// encoded from 'äø­å›½abc' -export const gbkFile = Uint8Array.from([214, 208, 185, 250, 97, 98, 99]); diff --git a/extensions/vscode-web-playground/src/extension.ts b/extensions/vscode-web-playground/src/extension.ts deleted file mode 100644 index 1d5df92bbcc..00000000000 --- a/extensions/vscode-web-playground/src/extension.ts +++ /dev/null @@ -1,3927 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// -// ############################################################################ -// -// ! USED FOR RUNNING VSCODE OUT OF SOURCES FOR WEB ! -// ! DO NOT REMOVE ! -// -// ############################################################################ -// - -import * as vscode from 'vscode'; -import { MemFS } from './memfs'; - -declare const navigator: unknown; - -export function activate(context: vscode.ExtensionContext) { - if (typeof navigator === 'object') { // do not run under node.js - const memFs = enableFs(context); - - if (vscode.workspace.workspaceFolders?.some(f => f.uri.scheme === MemFS.scheme)) { - memFs.seed(); - enableProblems(context); - enableTasks(); - enableDebug(context, memFs); - - vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(`memfs:/sample-folder/large.ts`)); - } - } -} - -function enableFs(context: vscode.ExtensionContext): MemFS { - const memFs = new MemFS(); - context.subscriptions.push(memFs); - - return memFs; -} - -function enableProblems(context: vscode.ExtensionContext): void { - const collection = vscode.languages.createDiagnosticCollection('test'); - if (vscode.window.activeTextEditor) { - updateDiagnostics(vscode.window.activeTextEditor.document, collection); - } - context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(editor => { - if (editor) { - updateDiagnostics(editor.document, collection); - } - })); -} - -function updateDiagnostics(document: vscode.TextDocument, collection: vscode.DiagnosticCollection): void { - if (document && document.fileName === '/sample-folder/large.ts') { - collection.set(document.uri, [{ - code: '', - message: 'cannot assign twice to immutable variable `storeHouses`', - range: new vscode.Range(new vscode.Position(4, 12), new vscode.Position(4, 32)), - severity: vscode.DiagnosticSeverity.Error, - source: '', - relatedInformation: [ - new vscode.DiagnosticRelatedInformation(new vscode.Location(document.uri, new vscode.Range(new vscode.Position(1, 8), new vscode.Position(1, 9))), 'first assignment to `x`') - ] - }, { - code: '', - message: 'function does not follow naming conventions', - range: new vscode.Range(new vscode.Position(7, 10), new vscode.Position(7, 23)), - severity: vscode.DiagnosticSeverity.Warning, - source: '' - }]); - } else { - collection.clear(); - } -} - -function enableTasks(): void { - - interface CustomBuildTaskDefinition extends vscode.TaskDefinition { - /** - * The build flavor. Should be either '32' or '64'. - */ - flavor: string; - - /** - * Additional build flags - */ - flags?: string[]; - } - - class CustomBuildTaskProvider implements vscode.TaskProvider { - static CustomBuildScriptType: string = 'custombuildscript'; - private tasks: vscode.Task[] | undefined; - - // We use a CustomExecution task when state needs to be shared accross runs of the task or when - // the task requires use of some VS Code API to run. - // If you don't need to share state between runs and if you don't need to execute VS Code API in your task, - // then a simple ShellExecution or ProcessExecution should be enough. - // Since our build has this shared state, the CustomExecution is used below. - private sharedState: string | undefined; - - constructor(private workspaceRoot: string) { } - - public async provideTasks(): Promise { - return this.getTasks(); - } - - public resolveTask(_task: vscode.Task): vscode.Task | undefined { - const flavor: string = _task.definition.flavor; - if (flavor) { - const definition: CustomBuildTaskDefinition = _task.definition; - return this.getTask(definition.flavor, definition.flags ? definition.flags : [], definition); - } - return undefined; - } - - private getTasks(): vscode.Task[] { - if (this.tasks !== undefined) { - return this.tasks; - } - // In our fictional build, we have two build flavors - const flavors: string[] = ['32', '64']; - // Each flavor can have some options. - const flags: string[][] = [['watch', 'incremental'], ['incremental'], []]; - - this.tasks = []; - flavors.forEach(flavor => { - flags.forEach(flagGroup => { - this.tasks!.push(this.getTask(flavor, flagGroup)); - }); - }); - return this.tasks; - } - - private getTask(flavor: string, flags: string[], definition?: CustomBuildTaskDefinition): vscode.Task { - if (definition === undefined) { - definition = { - type: CustomBuildTaskProvider.CustomBuildScriptType, - flavor, - flags - }; - } - return new vscode.Task2(definition, vscode.TaskScope.Workspace, `${flavor} ${flags.join(' ')}`, - CustomBuildTaskProvider.CustomBuildScriptType, new vscode.CustomExecution(async (): Promise => { - // When the task is executed, this callback will run. Here, we setup for running the task. - return new CustomBuildTaskTerminal(this.workspaceRoot, flavor, flags, () => this.sharedState, (state: string) => this.sharedState = state); - })); - } - } - - class CustomBuildTaskTerminal implements vscode.Pseudoterminal { - private writeEmitter = new vscode.EventEmitter(); - onDidWrite: vscode.Event = this.writeEmitter.event; - private closeEmitter = new vscode.EventEmitter(); - onDidClose?: vscode.Event = this.closeEmitter.event; - - private fileWatcher: vscode.FileSystemWatcher | undefined; - - constructor(private workspaceRoot: string, _flavor: string, private flags: string[], private getSharedState: () => string | undefined, private setSharedState: (state: string) => void) { - } - - open(_initialDimensions: vscode.TerminalDimensions | undefined): void { - // At this point we can start using the terminal. - if (this.flags.indexOf('watch') > -1) { - let pattern = this.workspaceRoot + '/customBuildFile'; - this.fileWatcher = vscode.workspace.createFileSystemWatcher(pattern); - this.fileWatcher.onDidChange(() => this.doBuild()); - this.fileWatcher.onDidCreate(() => this.doBuild()); - this.fileWatcher.onDidDelete(() => this.doBuild()); - } - this.doBuild(); - } - - close(): void { - // The terminal has been closed. Shutdown the build. - if (this.fileWatcher) { - this.fileWatcher.dispose(); - } - } - - private async doBuild(): Promise { - return new Promise((resolve) => { - this.writeEmitter.fire('Starting build...\r\n'); - let isIncremental = this.flags.indexOf('incremental') > -1; - if (isIncremental) { - if (this.getSharedState()) { - this.writeEmitter.fire('Using last build results: ' + this.getSharedState() + '\r\n'); - } else { - isIncremental = false; - this.writeEmitter.fire('No result from last build. Doing full build.\r\n'); - } - } - - // Since we don't actually build anything in this example set a timeout instead. - setTimeout(() => { - const date = new Date(); - this.setSharedState(date.toTimeString() + ' ' + date.toDateString()); - this.writeEmitter.fire('Build complete.\r\n\r\n'); - if (this.flags.indexOf('watch') === -1) { - this.closeEmitter.fire(); - resolve(); - } - }, isIncremental ? 1000 : 4000); - }); - } - } - - vscode.tasks.registerTaskProvider(CustomBuildTaskProvider.CustomBuildScriptType, new CustomBuildTaskProvider(vscode.workspace.rootPath!)); -} - -//--------------------------------------------------------------------------- -// DEBUG -//--------------------------------------------------------------------------- - -function enableDebug(context: vscode.ExtensionContext, memFs: MemFS): void { - context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('mock', new MockConfigurationProvider())); - context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory('mock', new MockDebugAdapterDescriptorFactory(memFs))); -} - -/** - * Declaration module describing the VS Code debug protocol. - * Auto-generated from json schema. Do not edit manually. - */ -declare module DebugProtocol { - - /** Base class of requests, responses, and events. */ - export interface ProtocolMessage { - /** Sequence number (also known as message ID). For protocol messages of type 'request' this ID can be used to cancel the request. */ - seq: number; - /** Message type. - Values: 'request', 'response', 'event', etc. - */ - type: string; - } - - /** A client or debug adapter initiated request. */ - export interface Request extends ProtocolMessage { - // type: 'request'; - /** The command to execute. */ - command: string; - /** Object containing arguments for the command. */ - arguments?: any; - } - - /** A debug adapter initiated event. */ - export interface Event extends ProtocolMessage { - // type: 'event'; - /** Type of event. */ - event: string; - /** Event-specific information. */ - body?: any; - } - - /** Response for a request. */ - export interface Response extends ProtocolMessage { - // type: 'response'; - /** Sequence number of the corresponding request. */ - request_seq: number; - /** Outcome of the request. - If true, the request was successful and the 'body' attribute may contain the result of the request. - If the value is false, the attribute 'message' contains the error in short form and the 'body' may contain additional information (see 'ErrorResponse.body.error'). - */ - success: boolean; - /** The command requested. */ - command: string; - /** Contains the raw error in short form if 'success' is false. - This raw error might be interpreted by the frontend and is not shown in the UI. - Some predefined values exist. - Values: - 'cancelled': request was cancelled. - etc. - */ - message?: string; - /** Contains request result if success is true and optional error details if success is false. */ - body?: any; - } - - /** On error (whenever 'success' is false), the body can provide more details. */ - export interface ErrorResponse extends Response { - body: { - /** An optional, structured error message. */ - error?: Message; - }; - } - - /** Cancel request; value of command field is 'cancel'. - The 'cancel' request is used by the frontend to indicate that it is no longer interested in the result produced by a specific request issued earlier. - This request has a hint characteristic: a debug adapter can only be expected to make a 'best effort' in honouring this request but there are no guarantees. - The 'cancel' request may return an error if it could not cancel an operation but a frontend should refrain from presenting this error to end users. - A frontend client should only call this request if the capability 'supportsCancelRequest' is true. - The request that got canceled still needs to send a response back. - This can either be a normal result ('success' attribute true) or an error response ('success' attribute false and the 'message' set to 'cancelled'). - Returning partial results from a cancelled request is possible but please note that a frontend client has no generic way for detecting that a response is partial or not. - */ - export interface CancelRequest extends Request { - // command: 'cancel'; - arguments?: CancelArguments; - } - - /** Arguments for 'cancel' request. */ - export interface CancelArguments { - /** The ID (attribute 'seq') of the request to cancel. */ - requestId?: number; - } - - /** Response to 'cancel' request. This is just an acknowledgement, so no body field is required. */ - export interface CancelResponse extends Response { - } - - /** Event message for 'initialized' event type. - This event indicates that the debug adapter is ready to accept configuration requests (e.g. SetBreakpointsRequest, SetExceptionBreakpointsRequest). - A debug adapter is expected to send this event when it is ready to accept configuration requests (but not before the 'initialize' request has finished). - The sequence of events/requests is as follows: - - adapters sends 'initialized' event (after the 'initialize' request has returned) - - frontend sends zero or more 'setBreakpoints' requests - - frontend sends one 'setFunctionBreakpoints' request - - frontend sends a 'setExceptionBreakpoints' request if one or more 'exceptionBreakpointFilters' have been defined (or if 'supportsConfigurationDoneRequest' is not defined or false) - - frontend sends other future configuration requests - - frontend sends one 'configurationDone' request to indicate the end of the configuration. - */ - export interface InitializedEvent extends Event { - // event: 'initialized'; - } - - /** Event message for 'stopped' event type. - The event indicates that the execution of the debuggee has stopped due to some condition. - This can be caused by a break point previously set, a stepping action has completed, by executing a debugger statement etc. - */ - export interface StoppedEvent extends Event { - // event: 'stopped'; - body: { - /** The reason for the event. - For backward compatibility this string is shown in the UI if the 'description' attribute is missing (but it must not be translated). - Values: 'step', 'breakpoint', 'exception', 'pause', 'entry', 'goto', 'function breakpoint', 'data breakpoint', etc. - */ - reason: string; - /** The full reason for the event, e.g. 'Paused on exception'. This string is shown in the UI as is and must be translated. */ - description?: string; - /** The thread which was stopped. */ - threadId?: number; - /** A value of true hints to the frontend that this event should not change the focus. */ - preserveFocusHint?: boolean; - /** Additional information. E.g. if reason is 'exception', text contains the exception name. This string is shown in the UI. */ - text?: string; - /** If 'allThreadsStopped' is true, a debug adapter can announce that all threads have stopped. - - The client should use this information to enable that all threads can be expanded to access their stacktraces. - - If the attribute is missing or false, only the thread with the given threadId can be expanded. - */ - allThreadsStopped?: boolean; - }; - } - - /** Event message for 'continued' event type. - The event indicates that the execution of the debuggee has continued. - Please note: a debug adapter is not expected to send this event in response to a request that implies that execution continues, e.g. 'launch' or 'continue'. - It is only necessary to send a 'continued' event if there was no previous request that implied this. - */ - export interface ContinuedEvent extends Event { - // event: 'continued'; - body: { - /** The thread which was continued. */ - threadId: number; - /** If 'allThreadsContinued' is true, a debug adapter can announce that all threads have continued. */ - allThreadsContinued?: boolean; - }; - } - - /** Event message for 'exited' event type. - The event indicates that the debuggee has exited and returns its exit code. - */ - export interface ExitedEvent extends Event { - // event: 'exited'; - body: { - /** The exit code returned from the debuggee. */ - exitCode: number; - }; - } - - /** Event message for 'terminated' event type. - The event indicates that debugging of the debuggee has terminated. This does **not** mean that the debuggee itself has exited. - */ - export interface TerminatedEvent extends Event { - // event: 'terminated'; - body?: { - /** A debug adapter may set 'restart' to true (or to an arbitrary object) to request that the front end restarts the session. - The value is not interpreted by the client and passed unmodified as an attribute '__restart' to the 'launch' and 'attach' requests. - */ - restart?: any; - }; - } - - /** Event message for 'thread' event type. - The event indicates that a thread has started or exited. - */ - export interface ThreadEvent extends Event { - // event: 'thread'; - body: { - /** The reason for the event. - Values: 'started', 'exited', etc. - */ - reason: string; - /** The identifier of the thread. */ - threadId: number; - }; - } - - /** Event message for 'output' event type. - The event indicates that the target has produced some output. - */ - export interface OutputEvent extends Event { - // event: 'output'; - body: { - /** The output category. If not specified, 'console' is assumed. - Values: 'console', 'stdout', 'stderr', 'telemetry', etc. - */ - category?: string; - /** The output to report. */ - output: string; - /** If an attribute 'variablesReference' exists and its value is > 0, the output contains objects which can be retrieved by passing 'variablesReference' to the 'variables' request. The value should be less than or equal to 2147483647 (2^31 - 1). */ - variablesReference?: number; - /** An optional source location where the output was produced. */ - source?: Source; - /** An optional source location line where the output was produced. */ - line?: number; - /** An optional source location column where the output was produced. */ - column?: number; - /** Optional data to report. For the 'telemetry' category the data will be sent to telemetry, for the other categories the data is shown in JSON format. */ - data?: any; - }; - } - - /** Event message for 'breakpoint' event type. - The event indicates that some information about a breakpoint has changed. - */ - export interface BreakpointEvent extends Event { - // event: 'breakpoint'; - body: { - /** The reason for the event. - Values: 'changed', 'new', 'removed', etc. - */ - reason: string; - /** The 'id' attribute is used to find the target breakpoint and the other attributes are used as the new values. */ - breakpoint: Breakpoint; - }; - } - - /** Event message for 'module' event type. - The event indicates that some information about a module has changed. - */ - export interface ModuleEvent extends Event { - // event: 'module'; - body: { - /** The reason for the event. */ - reason: 'new' | 'changed' | 'removed'; - /** The new, changed, or removed module. In case of 'removed' only the module id is used. */ - module: Module; - }; - } - - /** Event message for 'loadedSource' event type. - The event indicates that some source has been added, changed, or removed from the set of all loaded sources. - */ - export interface LoadedSourceEvent extends Event { - // event: 'loadedSource'; - body: { - /** The reason for the event. */ - reason: 'new' | 'changed' | 'removed'; - /** The new, changed, or removed source. */ - source: Source; - }; - } - - /** Event message for 'process' event type. - The event indicates that the debugger has begun debugging a new process. Either one that it has launched, or one that it has attached to. - */ - export interface ProcessEvent extends Event { - // event: 'process'; - body: { - /** The logical name of the process. This is usually the full path to process's executable file. Example: /home/example/myproj/program.js. */ - name: string; - /** The system process id of the debugged process. This property will be missing for non-system processes. */ - systemProcessId?: number; - /** If true, the process is running on the same computer as the debug adapter. */ - isLocalProcess?: boolean; - /** Describes how the debug engine started debugging this process. - 'launch': Process was launched under the debugger. - 'attach': Debugger attached to an existing process. - 'attachForSuspendedLaunch': A project launcher component has launched a new process in a suspended state and then asked the debugger to attach. - */ - startMethod?: 'launch' | 'attach' | 'attachForSuspendedLaunch'; - /** The size of a pointer or address for this process, in bits. This value may be used by clients when formatting addresses for display. */ - pointerSize?: number; - }; - } - - /** Event message for 'capabilities' event type. - The event indicates that one or more capabilities have changed. - Since the capabilities are dependent on the frontend and its UI, it might not be possible to change that at random times (or too late). - Consequently this event has a hint characteristic: a frontend can only be expected to make a 'best effort' in honouring individual capabilities but there are no guarantees. - Only changed capabilities need to be included, all other capabilities keep their values. - */ - export interface CapabilitiesEvent extends Event { - // event: 'capabilities'; - body: { - /** The set of updated capabilities. */ - capabilities: Capabilities; - }; - } - - /** RunInTerminal request; value of command field is 'runInTerminal'. - This request is sent from the debug adapter to the client to run a command in a terminal. This is typically used to launch the debuggee in a terminal provided by the client. - */ - export interface RunInTerminalRequest extends Request { - // command: 'runInTerminal'; - arguments: RunInTerminalRequestArguments; - } - - /** Arguments for 'runInTerminal' request. */ - export interface RunInTerminalRequestArguments { - /** What kind of terminal to launch. */ - kind?: 'integrated' | 'external'; - /** Optional title of the terminal. */ - title?: string; - /** Working directory of the command. */ - cwd: string; - /** List of arguments. The first argument is the command to run. */ - args: string[]; - /** Environment key-value pairs that are added to or removed from the default environment. */ - env?: { [key: string]: string | null; }; - } - - /** Response to 'runInTerminal' request. */ - export interface RunInTerminalResponse extends Response { - body: { - /** The process ID. The value should be less than or equal to 2147483647 (2^31 - 1). */ - processId?: number; - /** The process ID of the terminal shell. The value should be less than or equal to 2147483647 (2^31 - 1). */ - shellProcessId?: number; - }; - } - - /** Initialize request; value of command field is 'initialize'. - The 'initialize' request is sent as the first request from the client to the debug adapter in order to configure it with client capabilities and to retrieve capabilities from the debug adapter. - Until the debug adapter has responded to with an 'initialize' response, the client must not send any additional requests or events to the debug adapter. In addition the debug adapter is not allowed to send any requests or events to the client until it has responded with an 'initialize' response. - The 'initialize' request may only be sent once. - */ - export interface InitializeRequest extends Request { - // command: 'initialize'; - arguments: InitializeRequestArguments; - } - - /** Arguments for 'initialize' request. */ - export interface InitializeRequestArguments { - /** The ID of the (frontend) client using this adapter. */ - clientID?: string; - /** The human readable name of the (frontend) client using this adapter. */ - clientName?: string; - /** The ID of the debug adapter. */ - adapterID: string; - /** The ISO-639 locale of the (frontend) client using this adapter, e.g. en-US or de-CH. */ - locale?: string; - /** If true all line numbers are 1-based (default). */ - linesStartAt1?: boolean; - /** If true all column numbers are 1-based (default). */ - columnsStartAt1?: boolean; - /** Determines in what format paths are specified. The default is 'path', which is the native format. - Values: 'path', 'uri', etc. - */ - pathFormat?: string; - /** Client supports the optional type attribute for variables. */ - supportsVariableType?: boolean; - /** Client supports the paging of variables. */ - supportsVariablePaging?: boolean; - /** Client supports the runInTerminal request. */ - supportsRunInTerminalRequest?: boolean; - /** Client supports memory references. */ - supportsMemoryReferences?: boolean; - } - - /** Response to 'initialize' request. */ - export interface InitializeResponse extends Response { - /** The capabilities of this debug adapter. */ - body?: Capabilities; - } - - /** ConfigurationDone request; value of command field is 'configurationDone'. - The client of the debug protocol must send this request at the end of the sequence of configuration requests (which was started by the 'initialized' event). - */ - export interface ConfigurationDoneRequest extends Request { - // command: 'configurationDone'; - arguments?: ConfigurationDoneArguments; - } - - /** Arguments for 'configurationDone' request. */ - export interface ConfigurationDoneArguments { - } - - /** Response to 'configurationDone' request. This is just an acknowledgement, so no body field is required. */ - export interface ConfigurationDoneResponse extends Response { - } - - /** Launch request; value of command field is 'launch'. - The launch request is sent from the client to the debug adapter to start the debuggee with or without debugging (if 'noDebug' is true). Since launching is debugger/runtime specific, the arguments for this request are not part of this specification. - */ - export interface LaunchRequest extends Request { - // command: 'launch'; - arguments: LaunchRequestArguments; - } - - /** Arguments for 'launch' request. Additional attributes are implementation specific. */ - export interface LaunchRequestArguments { - /** If noDebug is true the launch request should launch the program without enabling debugging. */ - noDebug?: boolean; - /** Optional data from the previous, restarted session. - The data is sent as the 'restart' attribute of the 'terminated' event. - The client should leave the data intact. - */ - __restart?: any; - } - - /** Response to 'launch' request. This is just an acknowledgement, so no body field is required. */ - export interface LaunchResponse extends Response { - } - - /** Attach request; value of command field is 'attach'. - The attach request is sent from the client to the debug adapter to attach to a debuggee that is already running. Since attaching is debugger/runtime specific, the arguments for this request are not part of this specification. - */ - export interface AttachRequest extends Request { - // command: 'attach'; - arguments: AttachRequestArguments; - } - - /** Arguments for 'attach' request. Additional attributes are implementation specific. */ - export interface AttachRequestArguments { - /** Optional data from the previous, restarted session. - The data is sent as the 'restart' attribute of the 'terminated' event. - The client should leave the data intact. - */ - __restart?: any; - } - - /** Response to 'attach' request. This is just an acknowledgement, so no body field is required. */ - export interface AttachResponse extends Response { - } - - /** Restart request; value of command field is 'restart'. - Restarts a debug session. If the capability 'supportsRestartRequest' is missing or has the value false, - the client will implement 'restart' by terminating the debug adapter first and then launching it anew. - A debug adapter can override this default behaviour by implementing a restart request - and setting the capability 'supportsRestartRequest' to true. - */ - export interface RestartRequest extends Request { - // command: 'restart'; - arguments?: RestartArguments; - } - - /** Arguments for 'restart' request. */ - export interface RestartArguments { - } - - /** Response to 'restart' request. This is just an acknowledgement, so no body field is required. */ - export interface RestartResponse extends Response { - } - - /** Disconnect request; value of command field is 'disconnect'. - The 'disconnect' request is sent from the client to the debug adapter in order to stop debugging. It asks the debug adapter to disconnect from the debuggee and to terminate the debug adapter. If the debuggee has been started with the 'launch' request, the 'disconnect' request terminates the debuggee. If the 'attach' request was used to connect to the debuggee, 'disconnect' does not terminate the debuggee. This behavior can be controlled with the 'terminateDebuggee' argument (if supported by the debug adapter). - */ - export interface DisconnectRequest extends Request { - // command: 'disconnect'; - arguments?: DisconnectArguments; - } - - /** Arguments for 'disconnect' request. */ - export interface DisconnectArguments { - /** A value of true indicates that this 'disconnect' request is part of a restart sequence. */ - restart?: boolean; - /** Indicates whether the debuggee should be terminated when the debugger is disconnected. - If unspecified, the debug adapter is free to do whatever it thinks is best. - A client can only rely on this attribute being properly honored if a debug adapter returns true for the 'supportTerminateDebuggee' capability. - */ - terminateDebuggee?: boolean; - } - - /** Response to 'disconnect' request. This is just an acknowledgement, so no body field is required. */ - export interface DisconnectResponse extends Response { - } - - /** Terminate request; value of command field is 'terminate'. - The 'terminate' request is sent from the client to the debug adapter in order to give the debuggee a chance for terminating itself. - */ - export interface TerminateRequest extends Request { - // command: 'terminate'; - arguments?: TerminateArguments; - } - - /** Arguments for 'terminate' request. */ - export interface TerminateArguments { - /** A value of true indicates that this 'terminate' request is part of a restart sequence. */ - restart?: boolean; - } - - /** Response to 'terminate' request. This is just an acknowledgement, so no body field is required. */ - export interface TerminateResponse extends Response { - } - - /** BreakpointLocations request; value of command field is 'breakpointLocations'. - The 'breakpointLocations' request returns all possible locations for source breakpoints in a given range. - */ - export interface BreakpointLocationsRequest extends Request { - // command: 'breakpointLocations'; - arguments?: BreakpointLocationsArguments; - } - - /** Arguments for 'breakpointLocations' request. */ - export interface BreakpointLocationsArguments { - /** The source location of the breakpoints; either 'source.path' or 'source.reference' must be specified. */ - source: Source; - /** Start line of range to search possible breakpoint locations in. If only the line is specified, the request returns all possible locations in that line. */ - line: number; - /** Optional start column of range to search possible breakpoint locations in. If no start column is given, the first column in the start line is assumed. */ - column?: number; - /** Optional end line of range to search possible breakpoint locations in. If no end line is given, then the end line is assumed to be the start line. */ - endLine?: number; - /** Optional end column of range to search possible breakpoint locations in. If no end column is given, then it is assumed to be in the last column of the end line. */ - endColumn?: number; - } - - /** Response to 'breakpointLocations' request. - Contains possible locations for source breakpoints. - */ - export interface BreakpointLocationsResponse extends Response { - body: { - /** Sorted set of possible breakpoint locations. */ - breakpoints: BreakpointLocation[]; - }; - } - - /** SetBreakpoints request; value of command field is 'setBreakpoints'. - Sets multiple breakpoints for a single source and clears all previous breakpoints in that source. - To clear all breakpoint for a source, specify an empty array. - When a breakpoint is hit, a 'stopped' event (with reason 'breakpoint') is generated. - */ - export interface SetBreakpointsRequest extends Request { - // command: 'setBreakpoints'; - arguments: SetBreakpointsArguments; - } - - /** Arguments for 'setBreakpoints' request. */ - export interface SetBreakpointsArguments { - /** The source location of the breakpoints; either 'source.path' or 'source.reference' must be specified. */ - source: Source; - /** The code locations of the breakpoints. */ - breakpoints?: SourceBreakpoint[]; - /** Deprecated: The code locations of the breakpoints. */ - lines?: number[]; - /** A value of true indicates that the underlying source has been modified which results in new breakpoint locations. */ - sourceModified?: boolean; - } - - /** Response to 'setBreakpoints' request. - Returned is information about each breakpoint created by this request. - This includes the actual code location and whether the breakpoint could be verified. - The breakpoints returned are in the same order as the elements of the 'breakpoints' - (or the deprecated 'lines') array in the arguments. - */ - export interface SetBreakpointsResponse extends Response { - body: { - /** Information about the breakpoints. The array elements are in the same order as the elements of the 'breakpoints' (or the deprecated 'lines') array in the arguments. */ - breakpoints: Breakpoint[]; - }; - } - - /** SetFunctionBreakpoints request; value of command field is 'setFunctionBreakpoints'. - Replaces all existing function breakpoints with new function breakpoints. - To clear all function breakpoints, specify an empty array. - When a function breakpoint is hit, a 'stopped' event (with reason 'function breakpoint') is generated. - */ - export interface SetFunctionBreakpointsRequest extends Request { - // command: 'setFunctionBreakpoints'; - arguments: SetFunctionBreakpointsArguments; - } - - /** Arguments for 'setFunctionBreakpoints' request. */ - export interface SetFunctionBreakpointsArguments { - /** The function names of the breakpoints. */ - breakpoints: FunctionBreakpoint[]; - } - - /** Response to 'setFunctionBreakpoints' request. - Returned is information about each breakpoint created by this request. - */ - export interface SetFunctionBreakpointsResponse extends Response { - body: { - /** Information about the breakpoints. The array elements correspond to the elements of the 'breakpoints' array. */ - breakpoints: Breakpoint[]; - }; - } - - /** SetExceptionBreakpoints request; value of command field is 'setExceptionBreakpoints'. - The request configures the debuggers response to thrown exceptions. If an exception is configured to break, a 'stopped' event is fired (with reason 'exception'). - */ - export interface SetExceptionBreakpointsRequest extends Request { - // command: 'setExceptionBreakpoints'; - arguments: SetExceptionBreakpointsArguments; - } - - /** Arguments for 'setExceptionBreakpoints' request. */ - export interface SetExceptionBreakpointsArguments { - /** IDs of checked exception options. The set of IDs is returned via the 'exceptionBreakpointFilters' capability. */ - filters: string[]; - /** Configuration options for selected exceptions. */ - exceptionOptions?: ExceptionOptions[]; - } - - /** Response to 'setExceptionBreakpoints' request. This is just an acknowledgement, so no body field is required. */ - export interface SetExceptionBreakpointsResponse extends Response { - } - - /** DataBreakpointInfo request; value of command field is 'dataBreakpointInfo'. - Obtains information on a possible data breakpoint that could be set on an expression or variable. - */ - export interface DataBreakpointInfoRequest extends Request { - // command: 'dataBreakpointInfo'; - arguments: DataBreakpointInfoArguments; - } - - /** Arguments for 'dataBreakpointInfo' request. */ - export interface DataBreakpointInfoArguments { - /** Reference to the Variable container if the data breakpoint is requested for a child of the container. */ - variablesReference?: number; - /** The name of the Variable's child to obtain data breakpoint information for. If variableReference isnā€™t provided, this can be an expression. */ - name: string; - } - - /** Response to 'dataBreakpointInfo' request. */ - export interface DataBreakpointInfoResponse extends Response { - body: { - /** An identifier for the data on which a data breakpoint can be registered with the setDataBreakpoints request or null if no data breakpoint is available. */ - dataId: string | null; - /** UI string that describes on what data the breakpoint is set on or why a data breakpoint is not available. */ - description: string; - /** Optional attribute listing the available access types for a potential data breakpoint. A UI frontend could surface this information. */ - accessTypes?: DataBreakpointAccessType[]; - /** Optional attribute indicating that a potential data breakpoint could be persisted across sessions. */ - canPersist?: boolean; - }; - } - - /** SetDataBreakpoints request; value of command field is 'setDataBreakpoints'. - Replaces all existing data breakpoints with new data breakpoints. - To clear all data breakpoints, specify an empty array. - When a data breakpoint is hit, a 'stopped' event (with reason 'data breakpoint') is generated. - */ - export interface SetDataBreakpointsRequest extends Request { - // command: 'setDataBreakpoints'; - arguments: SetDataBreakpointsArguments; - } - - /** Arguments for 'setDataBreakpoints' request. */ - export interface SetDataBreakpointsArguments { - /** The contents of this array replaces all existing data breakpoints. An empty array clears all data breakpoints. */ - breakpoints: DataBreakpoint[]; - } - - /** Response to 'setDataBreakpoints' request. - Returned is information about each breakpoint created by this request. - */ - export interface SetDataBreakpointsResponse extends Response { - body: { - /** Information about the data breakpoints. The array elements correspond to the elements of the input argument 'breakpoints' array. */ - breakpoints: Breakpoint[]; - }; - } - - /** Continue request; value of command field is 'continue'. - The request starts the debuggee to run again. - */ - export interface ContinueRequest extends Request { - // command: 'continue'; - arguments: ContinueArguments; - } - - /** Arguments for 'continue' request. */ - export interface ContinueArguments { - /** Continue execution for the specified thread (if possible). If the backend cannot continue on a single thread but will continue on all threads, it should set the 'allThreadsContinued' attribute in the response to true. */ - threadId: number; - } - - /** Response to 'continue' request. */ - export interface ContinueResponse extends Response { - body: { - /** If true, the 'continue' request has ignored the specified thread and continued all threads instead. If this attribute is missing a value of 'true' is assumed for backward compatibility. */ - allThreadsContinued?: boolean; - }; - } - - /** Next request; value of command field is 'next'. - The request starts the debuggee to run again for one step. - The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. - */ - export interface NextRequest extends Request { - // command: 'next'; - arguments: NextArguments; - } - - /** Arguments for 'next' request. */ - export interface NextArguments { - /** Execute 'next' for this thread. */ - threadId: number; - } - - /** Response to 'next' request. This is just an acknowledgement, so no body field is required. */ - export interface NextResponse extends Response { - } - - /** StepIn request; value of command field is 'stepIn'. - The request starts the debuggee to step into a function/method if possible. - If it cannot step into a target, 'stepIn' behaves like 'next'. - The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. - If there are multiple function/method calls (or other targets) on the source line, - the optional argument 'targetId' can be used to control into which target the 'stepIn' should occur. - The list of possible targets for a given source line can be retrieved via the 'stepInTargets' request. - */ - export interface StepInRequest extends Request { - // command: 'stepIn'; - arguments: StepInArguments; - } - - /** Arguments for 'stepIn' request. */ - export interface StepInArguments { - /** Execute 'stepIn' for this thread. */ - threadId: number; - /** Optional id of the target to step into. */ - targetId?: number; - } - - /** Response to 'stepIn' request. This is just an acknowledgement, so no body field is required. */ - export interface StepInResponse extends Response { - } - - /** StepOut request; value of command field is 'stepOut'. - The request starts the debuggee to run again for one step. - The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. - */ - export interface StepOutRequest extends Request { - // command: 'stepOut'; - arguments: StepOutArguments; - } - - /** Arguments for 'stepOut' request. */ - export interface StepOutArguments { - /** Execute 'stepOut' for this thread. */ - threadId: number; - } - - /** Response to 'stepOut' request. This is just an acknowledgement, so no body field is required. */ - export interface StepOutResponse extends Response { - } - - /** StepBack request; value of command field is 'stepBack'. - The request starts the debuggee to run one step backwards. - The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. Clients should only call this request if the capability 'supportsStepBack' is true. - */ - export interface StepBackRequest extends Request { - // command: 'stepBack'; - arguments: StepBackArguments; - } - - /** Arguments for 'stepBack' request. */ - export interface StepBackArguments { - /** Execute 'stepBack' for this thread. */ - threadId: number; - } - - /** Response to 'stepBack' request. This is just an acknowledgement, so no body field is required. */ - export interface StepBackResponse extends Response { - } - - /** ReverseContinue request; value of command field is 'reverseContinue'. - The request starts the debuggee to run backward. Clients should only call this request if the capability 'supportsStepBack' is true. - */ - export interface ReverseContinueRequest extends Request { - // command: 'reverseContinue'; - arguments: ReverseContinueArguments; - } - - /** Arguments for 'reverseContinue' request. */ - export interface ReverseContinueArguments { - /** Execute 'reverseContinue' for this thread. */ - threadId: number; - } - - /** Response to 'reverseContinue' request. This is just an acknowledgement, so no body field is required. */ - export interface ReverseContinueResponse extends Response { - } - - /** RestartFrame request; value of command field is 'restartFrame'. - The request restarts execution of the specified stackframe. - The debug adapter first sends the response and then a 'stopped' event (with reason 'restart') after the restart has completed. - */ - export interface RestartFrameRequest extends Request { - // command: 'restartFrame'; - arguments: RestartFrameArguments; - } - - /** Arguments for 'restartFrame' request. */ - export interface RestartFrameArguments { - /** Restart this stackframe. */ - frameId: number; - } - - /** Response to 'restartFrame' request. This is just an acknowledgement, so no body field is required. */ - export interface RestartFrameResponse extends Response { - } - - /** Goto request; value of command field is 'goto'. - The request sets the location where the debuggee will continue to run. - This makes it possible to skip the execution of code or to executed code again. - The code between the current location and the goto target is not executed but skipped. - The debug adapter first sends the response and then a 'stopped' event with reason 'goto'. - */ - export interface GotoRequest extends Request { - // command: 'goto'; - arguments: GotoArguments; - } - - /** Arguments for 'goto' request. */ - export interface GotoArguments { - /** Set the goto target for this thread. */ - threadId: number; - /** The location where the debuggee will continue to run. */ - targetId: number; - } - - /** Response to 'goto' request. This is just an acknowledgement, so no body field is required. */ - export interface GotoResponse extends Response { - } - - /** Pause request; value of command field is 'pause'. - The request suspends the debuggee. - The debug adapter first sends the response and then a 'stopped' event (with reason 'pause') after the thread has been paused successfully. - */ - export interface PauseRequest extends Request { - // command: 'pause'; - arguments: PauseArguments; - } - - /** Arguments for 'pause' request. */ - export interface PauseArguments { - /** Pause execution for this thread. */ - threadId: number; - } - - /** Response to 'pause' request. This is just an acknowledgement, so no body field is required. */ - export interface PauseResponse extends Response { - } - - /** StackTrace request; value of command field is 'stackTrace'. - The request returns a stacktrace from the current execution state. - */ - export interface StackTraceRequest extends Request { - // command: 'stackTrace'; - arguments: StackTraceArguments; - } - - /** Arguments for 'stackTrace' request. */ - export interface StackTraceArguments { - /** Retrieve the stacktrace for this thread. */ - threadId: number; - /** The index of the first frame to return; if omitted frames start at 0. */ - startFrame?: number; - /** The maximum number of frames to return. If levels is not specified or 0, all frames are returned. */ - levels?: number; - /** Specifies details on how to format the stack frames. */ - format?: StackFrameFormat; - } - - /** Response to 'stackTrace' request. */ - export interface StackTraceResponse extends Response { - body: { - /** The frames of the stackframe. If the array has length zero, there are no stackframes available. - This means that there is no location information available. - */ - stackFrames: StackFrame[]; - /** The total number of frames available. */ - totalFrames?: number; - }; - } - - /** Scopes request; value of command field is 'scopes'. - The request returns the variable scopes for a given stackframe ID. - */ - export interface ScopesRequest extends Request { - // command: 'scopes'; - arguments: ScopesArguments; - } - - /** Arguments for 'scopes' request. */ - export interface ScopesArguments { - /** Retrieve the scopes for this stackframe. */ - frameId: number; - } - - /** Response to 'scopes' request. */ - export interface ScopesResponse extends Response { - body: { - /** The scopes of the stackframe. If the array has length zero, there are no scopes available. */ - scopes: Scope[]; - }; - } - - /** Variables request; value of command field is 'variables'. - Retrieves all child variables for the given variable reference. - An optional filter can be used to limit the fetched children to either named or indexed children. - */ - export interface VariablesRequest extends Request { - // command: 'variables'; - arguments: VariablesArguments; - } - - /** Arguments for 'variables' request. */ - export interface VariablesArguments { - /** The Variable reference. */ - variablesReference: number; - /** Optional filter to limit the child variables to either named or indexed. If omitted, both types are fetched. */ - filter?: 'indexed' | 'named'; - /** The index of the first variable to return; if omitted children start at 0. */ - start?: number; - /** The number of variables to return. If count is missing or 0, all variables are returned. */ - count?: number; - /** Specifies details on how to format the Variable values. */ - format?: ValueFormat; - } - - /** Response to 'variables' request. */ - export interface VariablesResponse extends Response { - body: { - /** All (or a range) of variables for the given variable reference. */ - variables: Variable[]; - }; - } - - /** SetVariable request; value of command field is 'setVariable'. - Set the variable with the given name in the variable container to a new value. - */ - export interface SetVariableRequest extends Request { - // command: 'setVariable'; - arguments: SetVariableArguments; - } - - /** Arguments for 'setVariable' request. */ - export interface SetVariableArguments { - /** The reference of the variable container. */ - variablesReference: number; - /** The name of the variable in the container. */ - name: string; - /** The value of the variable. */ - value: string; - /** Specifies details on how to format the response value. */ - format?: ValueFormat; - } - - /** Response to 'setVariable' request. */ - export interface SetVariableResponse extends Response { - body: { - /** The new value of the variable. */ - value: string; - /** The type of the new value. Typically shown in the UI when hovering over the value. */ - type?: string; - /** If variablesReference is > 0, the new value is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. The value should be less than or equal to 2147483647 (2^31 - 1). */ - variablesReference?: number; - /** The number of named child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). - */ - namedVariables?: number; - /** The number of indexed child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). - */ - indexedVariables?: number; - }; - } - - /** Source request; value of command field is 'source'. - The request retrieves the source code for a given source reference. - */ - export interface SourceRequest extends Request { - // command: 'source'; - arguments: SourceArguments; - } - - /** Arguments for 'source' request. */ - export interface SourceArguments { - /** Specifies the source content to load. Either source.path or source.sourceReference must be specified. */ - source?: Source; - /** The reference to the source. This is the same as source.sourceReference. This is provided for backward compatibility since old backends do not understand the 'source' attribute. */ - sourceReference: number; - } - - /** Response to 'source' request. */ - export interface SourceResponse extends Response { - body: { - /** Content of the source reference. */ - content: string; - /** Optional content type (mime type) of the source. */ - mimeType?: string; - }; - } - - /** Threads request; value of command field is 'threads'. - The request retrieves a list of all threads. - */ - export interface ThreadsRequest extends Request { - // command: 'threads'; - } - - /** Response to 'threads' request. */ - export interface ThreadsResponse extends Response { - body: { - /** All threads. */ - threads: Thread[]; - }; - } - - /** TerminateThreads request; value of command field is 'terminateThreads'. - The request terminates the threads with the given ids. - */ - export interface TerminateThreadsRequest extends Request { - // command: 'terminateThreads'; - arguments: TerminateThreadsArguments; - } - - /** Arguments for 'terminateThreads' request. */ - export interface TerminateThreadsArguments { - /** Ids of threads to be terminated. */ - threadIds?: number[]; - } - - /** Response to 'terminateThreads' request. This is just an acknowledgement, so no body field is required. */ - export interface TerminateThreadsResponse extends Response { - } - - /** Modules request; value of command field is 'modules'. - Modules can be retrieved from the debug adapter with the ModulesRequest which can either return all modules or a range of modules to support paging. - */ - export interface ModulesRequest extends Request { - // command: 'modules'; - arguments: ModulesArguments; - } - - /** Arguments for 'modules' request. */ - export interface ModulesArguments { - /** The index of the first module to return; if omitted modules start at 0. */ - startModule?: number; - /** The number of modules to return. If moduleCount is not specified or 0, all modules are returned. */ - moduleCount?: number; - } - - /** Response to 'modules' request. */ - export interface ModulesResponse extends Response { - body: { - /** All modules or range of modules. */ - modules: Module[]; - /** The total number of modules available. */ - totalModules?: number; - }; - } - - /** LoadedSources request; value of command field is 'loadedSources'. - Retrieves the set of all sources currently loaded by the debugged process. - */ - export interface LoadedSourcesRequest extends Request { - // command: 'loadedSources'; - arguments?: LoadedSourcesArguments; - } - - /** Arguments for 'loadedSources' request. */ - export interface LoadedSourcesArguments { - } - - /** Response to 'loadedSources' request. */ - export interface LoadedSourcesResponse extends Response { - body: { - /** Set of loaded sources. */ - sources: Source[]; - }; - } - - /** Evaluate request; value of command field is 'evaluate'. - Evaluates the given expression in the context of the top most stack frame. - The expression has access to any variables and arguments that are in scope. - */ - export interface EvaluateRequest extends Request { - // command: 'evaluate'; - arguments: EvaluateArguments; - } - - /** Arguments for 'evaluate' request. */ - export interface EvaluateArguments { - /** The expression to evaluate. */ - expression: string; - /** Evaluate the expression in the scope of this stack frame. If not specified, the expression is evaluated in the global scope. */ - frameId?: number; - /** The context in which the evaluate request is run. - Values: - 'watch': evaluate is run in a watch. - 'repl': evaluate is run from REPL console. - 'hover': evaluate is run from a data hover. - etc. - */ - context?: string; - /** Specifies details on how to format the Evaluate result. */ - format?: ValueFormat; - } - - /** Response to 'evaluate' request. */ - export interface EvaluateResponse extends Response { - body: { - /** The result of the evaluate request. */ - result: string; - /** The optional type of the evaluate result. */ - type?: string; - /** Properties of a evaluate result that can be used to determine how to render the result in the UI. */ - presentationHint?: VariablePresentationHint; - /** If variablesReference is > 0, the evaluate result is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. The value should be less than or equal to 2147483647 (2^31 - 1). */ - variablesReference: number; - /** The number of named child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). - */ - namedVariables?: number; - /** The number of indexed child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). - */ - indexedVariables?: number; - /** Memory reference to a location appropriate for this result. For pointer type eval results, this is generally a reference to the memory address contained in the pointer. */ - memoryReference?: string; - }; - } - - /** SetExpression request; value of command field is 'setExpression'. - Evaluates the given 'value' expression and assigns it to the 'expression' which must be a modifiable l-value. - The expressions have access to any variables and arguments that are in scope of the specified frame. - */ - export interface SetExpressionRequest extends Request { - // command: 'setExpression'; - arguments: SetExpressionArguments; - } - - /** Arguments for 'setExpression' request. */ - export interface SetExpressionArguments { - /** The l-value expression to assign to. */ - expression: string; - /** The value expression to assign to the l-value expression. */ - value: string; - /** Evaluate the expressions in the scope of this stack frame. If not specified, the expressions are evaluated in the global scope. */ - frameId?: number; - /** Specifies how the resulting value should be formatted. */ - format?: ValueFormat; - } - - /** Response to 'setExpression' request. */ - export interface SetExpressionResponse extends Response { - body: { - /** The new value of the expression. */ - value: string; - /** The optional type of the value. */ - type?: string; - /** Properties of a value that can be used to determine how to render the result in the UI. */ - presentationHint?: VariablePresentationHint; - /** If variablesReference is > 0, the value is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. The value should be less than or equal to 2147483647 (2^31 - 1). */ - variablesReference?: number; - /** The number of named child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). - */ - namedVariables?: number; - /** The number of indexed child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). - */ - indexedVariables?: number; - }; - } - - /** StepInTargets request; value of command field is 'stepInTargets'. - This request retrieves the possible stepIn targets for the specified stack frame. - These targets can be used in the 'stepIn' request. - The StepInTargets may only be called if the 'supportsStepInTargetsRequest' capability exists and is true. - */ - export interface StepInTargetsRequest extends Request { - // command: 'stepInTargets'; - arguments: StepInTargetsArguments; - } - - /** Arguments for 'stepInTargets' request. */ - export interface StepInTargetsArguments { - /** The stack frame for which to retrieve the possible stepIn targets. */ - frameId: number; - } - - /** Response to 'stepInTargets' request. */ - export interface StepInTargetsResponse extends Response { - body: { - /** The possible stepIn targets of the specified source location. */ - targets: StepInTarget[]; - }; - } - - /** GotoTargets request; value of command field is 'gotoTargets'. - This request retrieves the possible goto targets for the specified source location. - These targets can be used in the 'goto' request. - The GotoTargets request may only be called if the 'supportsGotoTargetsRequest' capability exists and is true. - */ - export interface GotoTargetsRequest extends Request { - // command: 'gotoTargets'; - arguments: GotoTargetsArguments; - } - - /** Arguments for 'gotoTargets' request. */ - export interface GotoTargetsArguments { - /** The source location for which the goto targets are determined. */ - source: Source; - /** The line location for which the goto targets are determined. */ - line: number; - /** An optional column location for which the goto targets are determined. */ - column?: number; - } - - /** Response to 'gotoTargets' request. */ - export interface GotoTargetsResponse extends Response { - body: { - /** The possible goto targets of the specified location. */ - targets: GotoTarget[]; - }; - } - - /** Completions request; value of command field is 'completions'. - Returns a list of possible completions for a given caret position and text. - The CompletionsRequest may only be called if the 'supportsCompletionsRequest' capability exists and is true. - */ - export interface CompletionsRequest extends Request { - // command: 'completions'; - arguments: CompletionsArguments; - } - - /** Arguments for 'completions' request. */ - export interface CompletionsArguments { - /** Returns completions in the scope of this stack frame. If not specified, the completions are returned for the global scope. */ - frameId?: number; - /** One or more source lines. Typically this is the text a user has typed into the debug console before he asked for completion. */ - text: string; - /** The character position for which to determine the completion proposals. */ - column: number; - /** An optional line for which to determine the completion proposals. If missing the first line of the text is assumed. */ - line?: number; - } - - /** Response to 'completions' request. */ - export interface CompletionsResponse extends Response { - body: { - /** The possible completions for . */ - targets: CompletionItem[]; - }; - } - - /** ExceptionInfo request; value of command field is 'exceptionInfo'. - Retrieves the details of the exception that caused this event to be raised. - */ - export interface ExceptionInfoRequest extends Request { - // command: 'exceptionInfo'; - arguments: ExceptionInfoArguments; - } - - /** Arguments for 'exceptionInfo' request. */ - export interface ExceptionInfoArguments { - /** Thread for which exception information should be retrieved. */ - threadId: number; - } - - /** Response to 'exceptionInfo' request. */ - export interface ExceptionInfoResponse extends Response { - body: { - /** ID of the exception that was thrown. */ - exceptionId: string; - /** Descriptive text for the exception provided by the debug adapter. */ - description?: string; - /** Mode that caused the exception notification to be raised. */ - breakMode: ExceptionBreakMode; - /** Detailed information about the exception. */ - details?: ExceptionDetails; - }; - } - - /** ReadMemory request; value of command field is 'readMemory'. - Reads bytes from memory at the provided location. - */ - export interface ReadMemoryRequest extends Request { - // command: 'readMemory'; - arguments: ReadMemoryArguments; - } - - /** Arguments for 'readMemory' request. */ - export interface ReadMemoryArguments { - /** Memory reference to the base location from which data should be read. */ - memoryReference: string; - /** Optional offset (in bytes) to be applied to the reference location before reading data. Can be negative. */ - offset?: number; - /** Number of bytes to read at the specified location and offset. */ - count: number; - } - - /** Response to 'readMemory' request. */ - export interface ReadMemoryResponse extends Response { - body?: { - /** The address of the first byte of data returned. Treated as a hex value if prefixed with '0x', or as a decimal value otherwise. */ - address: string; - /** The number of unreadable bytes encountered after the last successfully read byte. This can be used to determine the number of bytes that must be skipped before a subsequent 'readMemory' request will succeed. */ - unreadableBytes?: number; - /** The bytes read from memory, encoded using base64. */ - data?: string; - }; - } - - /** Disassemble request; value of command field is 'disassemble'. - Disassembles code stored at the provided location. - */ - export interface DisassembleRequest extends Request { - // command: 'disassemble'; - arguments: DisassembleArguments; - } - - /** Arguments for 'disassemble' request. */ - export interface DisassembleArguments { - /** Memory reference to the base location containing the instructions to disassemble. */ - memoryReference: string; - /** Optional offset (in bytes) to be applied to the reference location before disassembling. Can be negative. */ - offset?: number; - /** Optional offset (in instructions) to be applied after the byte offset (if any) before disassembling. Can be negative. */ - instructionOffset?: number; - /** Number of instructions to disassemble starting at the specified location and offset. An adapter must return exactly this number of instructions - any unavailable instructions should be replaced with an implementation-defined 'invalid instruction' value. */ - instructionCount: number; - /** If true, the adapter should attempt to resolve memory addresses and other values to symbolic names. */ - resolveSymbols?: boolean; - } - - /** Response to 'disassemble' request. */ - export interface DisassembleResponse extends Response { - body?: { - /** The list of disassembled instructions. */ - instructions: DisassembledInstruction[]; - }; - } - - /** Information about the capabilities of a debug adapter. */ - export interface Capabilities { - /** The debug adapter supports the 'configurationDone' request. */ - supportsConfigurationDoneRequest?: boolean; - /** The debug adapter supports function breakpoints. */ - supportsFunctionBreakpoints?: boolean; - /** The debug adapter supports conditional breakpoints. */ - supportsConditionalBreakpoints?: boolean; - /** The debug adapter supports breakpoints that break execution after a specified number of hits. */ - supportsHitConditionalBreakpoints?: boolean; - /** The debug adapter supports a (side effect free) evaluate request for data hovers. */ - supportsEvaluateForHovers?: boolean; - /** Available filters or options for the setExceptionBreakpoints request. */ - exceptionBreakpointFilters?: ExceptionBreakpointsFilter[]; - /** The debug adapter supports stepping back via the 'stepBack' and 'reverseContinue' requests. */ - supportsStepBack?: boolean; - /** The debug adapter supports setting a variable to a value. */ - supportsSetVariable?: boolean; - /** The debug adapter supports restarting a frame. */ - supportsRestartFrame?: boolean; - /** The debug adapter supports the 'gotoTargets' request. */ - supportsGotoTargetsRequest?: boolean; - /** The debug adapter supports the 'stepInTargets' request. */ - supportsStepInTargetsRequest?: boolean; - /** The debug adapter supports the 'completions' request. */ - supportsCompletionsRequest?: boolean; - /** The set of characters that should trigger completion in a REPL. If not specified, the UI should assume the '.' character. */ - completionTriggerCharacters?: string[]; - /** The debug adapter supports the 'modules' request. */ - supportsModulesRequest?: boolean; - /** The set of additional module information exposed by the debug adapter. */ - additionalModuleColumns?: ColumnDescriptor[]; - /** Checksum algorithms supported by the debug adapter. */ - supportedChecksumAlgorithms?: ChecksumAlgorithm[]; - /** The debug adapter supports the 'restart' request. In this case a client should not implement 'restart' by terminating and relaunching the adapter but by calling the RestartRequest. */ - supportsRestartRequest?: boolean; - /** The debug adapter supports 'exceptionOptions' on the setExceptionBreakpoints request. */ - supportsExceptionOptions?: boolean; - /** The debug adapter supports a 'format' attribute on the stackTraceRequest, variablesRequest, and evaluateRequest. */ - supportsValueFormattingOptions?: boolean; - /** The debug adapter supports the 'exceptionInfo' request. */ - supportsExceptionInfoRequest?: boolean; - /** The debug adapter supports the 'terminateDebuggee' attribute on the 'disconnect' request. */ - supportTerminateDebuggee?: boolean; - /** The debug adapter supports the delayed loading of parts of the stack, which requires that both the 'startFrame' and 'levels' arguments and the 'totalFrames' result of the 'StackTrace' request are supported. */ - supportsDelayedStackTraceLoading?: boolean; - /** The debug adapter supports the 'loadedSources' request. */ - supportsLoadedSourcesRequest?: boolean; - /** The debug adapter supports logpoints by interpreting the 'logMessage' attribute of the SourceBreakpoint. */ - supportsLogPoints?: boolean; - /** The debug adapter supports the 'terminateThreads' request. */ - supportsTerminateThreadsRequest?: boolean; - /** The debug adapter supports the 'setExpression' request. */ - supportsSetExpression?: boolean; - /** The debug adapter supports the 'terminate' request. */ - supportsTerminateRequest?: boolean; - /** The debug adapter supports data breakpoints. */ - supportsDataBreakpoints?: boolean; - /** The debug adapter supports the 'readMemory' request. */ - supportsReadMemoryRequest?: boolean; - /** The debug adapter supports the 'disassemble' request. */ - supportsDisassembleRequest?: boolean; - /** The debug adapter supports the 'cancel' request. */ - supportsCancelRequest?: boolean; - /** The debug adapter supports the 'breakpointLocations' request. */ - supportsBreakpointLocationsRequest?: boolean; - } - - /** An ExceptionBreakpointsFilter is shown in the UI as an option for configuring how exceptions are dealt with. */ - export interface ExceptionBreakpointsFilter { - /** The internal ID of the filter. This value is passed to the setExceptionBreakpoints request. */ - filter: string; - /** The name of the filter. This will be shown in the UI. */ - label: string; - /** Initial value of the filter. If not specified a value 'false' is assumed. */ - default?: boolean; - } - - /** A structured message object. Used to return errors from requests. */ - export interface Message { - /** Unique identifier for the message. */ - id: number; - /** A format string for the message. Embedded variables have the form '{name}'. - If variable name starts with an underscore character, the variable does not contain user data (PII) and can be safely used for telemetry purposes. - */ - format: string; - /** An object used as a dictionary for looking up the variables in the format string. */ - variables?: { [key: string]: string; }; - /** If true send to telemetry. */ - sendTelemetry?: boolean; - /** If true show user. */ - showUser?: boolean; - /** An optional url where additional information about this message can be found. */ - url?: string; - /** An optional label that is presented to the user as the UI for opening the url. */ - urlLabel?: string; - } - - /** A Module object represents a row in the modules view. - Two attributes are mandatory: an id identifies a module in the modules view and is used in a ModuleEvent for identifying a module for adding, updating or deleting. - The name is used to minimally render the module in the UI. - - Additional attributes can be added to the module. They will show up in the module View if they have a corresponding ColumnDescriptor. - - To avoid an unnecessary proliferation of additional attributes with similar semantics but different names - we recommend to re-use attributes from the 'recommended' list below first, and only introduce new attributes if nothing appropriate could be found. - */ - export interface Module { - /** Unique identifier for the module. */ - id: number | string; - /** A name of the module. */ - name: string; - /** optional but recommended attributes. - always try to use these first before introducing additional attributes. - - Logical full path to the module. The exact definition is implementation defined, but usually this would be a full path to the on-disk file for the module. - */ - path?: string; - /** True if the module is optimized. */ - isOptimized?: boolean; - /** True if the module is considered 'user code' by a debugger that supports 'Just My Code'. */ - isUserCode?: boolean; - /** Version of Module. */ - version?: string; - /** User understandable description of if symbols were found for the module (ex: 'Symbols Loaded', 'Symbols not found', etc. */ - symbolStatus?: string; - /** Logical full path to the symbol file. The exact definition is implementation defined. */ - symbolFilePath?: string; - /** Module created or modified. */ - dateTimeStamp?: string; - /** Address range covered by this module. */ - addressRange?: string; - } - - /** A ColumnDescriptor specifies what module attribute to show in a column of the ModulesView, how to format it, and what the column's label should be. - It is only used if the underlying UI actually supports this level of customization. - */ - export interface ColumnDescriptor { - /** Name of the attribute rendered in this column. */ - attributeName: string; - /** Header UI label of column. */ - label: string; - /** Format to use for the rendered values in this column. TBD how the format strings looks like. */ - format?: string; - /** Datatype of values in this column. Defaults to 'string' if not specified. */ - type?: 'string' | 'number' | 'boolean' | 'unixTimestampUTC'; - /** Width of this column in characters (hint only). */ - width?: number; - } - - /** The ModulesViewDescriptor is the container for all declarative configuration options of a ModuleView. - For now it only specifies the columns to be shown in the modules view. - */ - export interface ModulesViewDescriptor { - columns: ColumnDescriptor[]; - } - - /** A Thread */ - export interface Thread { - /** Unique identifier for the thread. */ - id: number; - /** A name of the thread. */ - name: string; - } - - /** A Source is a descriptor for source code. It is returned from the debug adapter as part of a StackFrame and it is used by clients when specifying breakpoints. */ - export interface Source { - /** The short name of the source. Every source returned from the debug adapter has a name. When sending a source to the debug adapter this name is optional. */ - name?: string; - /** The path of the source to be shown in the UI. It is only used to locate and load the content of the source if no sourceReference is specified (or its value is 0). */ - path?: string; - /** If sourceReference > 0 the contents of the source must be retrieved through the SourceRequest (even if a path is specified). A sourceReference is only valid for a session, so it must not be used to persist a source. The value should be less than or equal to 2147483647 (2^31 - 1). */ - sourceReference?: number; - /** An optional hint for how to present the source in the UI. A value of 'deemphasize' can be used to indicate that the source is not available or that it is skipped on stepping. */ - presentationHint?: 'normal' | 'emphasize' | 'deemphasize'; - /** The (optional) origin of this source: possible values 'internal module', 'inlined content from source map', etc. */ - origin?: string; - /** An optional list of sources that are related to this source. These may be the source that generated this source. */ - sources?: Source[]; - /** Optional data that a debug adapter might want to loop through the client. The client should leave the data intact and persist it across sessions. The client should not interpret the data. */ - adapterData?: any; - /** The checksums associated with this file. */ - checksums?: Checksum[]; - } - - /** A Stackframe contains the source location. */ - export interface StackFrame { - /** An identifier for the stack frame. It must be unique across all threads. This id can be used to retrieve the scopes of the frame with the 'scopesRequest' or to restart the execution of a stackframe. */ - id: number; - /** The name of the stack frame, typically a method name. */ - name: string; - /** The optional source of the frame. */ - source?: Source; - /** The line within the file of the frame. If source is null or doesn't exist, line is 0 and must be ignored. */ - line: number; - /** The column within the line. If source is null or doesn't exist, column is 0 and must be ignored. */ - column: number; - /** An optional end line of the range covered by the stack frame. */ - endLine?: number; - /** An optional end column of the range covered by the stack frame. */ - endColumn?: number; - /** Optional memory reference for the current instruction pointer in this frame. */ - instructionPointerReference?: string; - /** The module associated with this frame, if any. */ - moduleId?: number | string; - /** An optional hint for how to present this frame in the UI. A value of 'label' can be used to indicate that the frame is an artificial frame that is used as a visual label or separator. A value of 'subtle' can be used to change the appearance of a frame in a 'subtle' way. */ - presentationHint?: 'normal' | 'label' | 'subtle'; - } - - /** A Scope is a named container for variables. Optionally a scope can map to a source or a range within a source. */ - export interface Scope { - /** Name of the scope such as 'Arguments', 'Locals', or 'Registers'. This string is shown in the UI as is and can be translated. */ - name: string; - /** An optional hint for how to present this scope in the UI. If this attribute is missing, the scope is shown with a generic UI. - Values: - 'arguments': Scope contains method arguments. - 'locals': Scope contains local variables. - 'registers': Scope contains registers. Only a single 'registers' scope should be returned from a 'scopes' request. - etc. - */ - presentationHint?: string; - /** The variables of this scope can be retrieved by passing the value of variablesReference to the VariablesRequest. */ - variablesReference: number; - /** The number of named variables in this scope. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. - */ - namedVariables?: number; - /** The number of indexed variables in this scope. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. - */ - indexedVariables?: number; - /** If true, the number of variables in this scope is large or expensive to retrieve. */ - expensive: boolean; - /** Optional source for this scope. */ - source?: Source; - /** Optional start line of the range covered by this scope. */ - line?: number; - /** Optional start column of the range covered by this scope. */ - column?: number; - /** Optional end line of the range covered by this scope. */ - endLine?: number; - /** Optional end column of the range covered by this scope. */ - endColumn?: number; - } - - /** A Variable is a name/value pair. - Optionally a variable can have a 'type' that is shown if space permits or when hovering over the variable's name. - An optional 'kind' is used to render additional properties of the variable, e.g. different icons can be used to indicate that a variable is public or private. - If the value is structured (has children), a handle is provided to retrieve the children with the VariablesRequest. - If the number of named or indexed children is large, the numbers should be returned via the optional 'namedVariables' and 'indexedVariables' attributes. - The client can use this optional information to present the children in a paged UI and fetch them in chunks. - */ - export interface Variable { - /** The variable's name. */ - name: string; - /** The variable's value. This can be a multi-line text, e.g. for a function the body of a function. */ - value: string; - /** The type of the variable's value. Typically shown in the UI when hovering over the value. */ - type?: string; - /** Properties of a variable that can be used to determine how to render the variable in the UI. */ - presentationHint?: VariablePresentationHint; - /** Optional evaluatable name of this variable which can be passed to the 'EvaluateRequest' to fetch the variable's value. */ - evaluateName?: string; - /** If variablesReference is > 0, the variable is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. */ - variablesReference: number; - /** The number of named child variables. - The client can use this optional information to present the children in a paged UI and fetch them in chunks. - */ - namedVariables?: number; - /** The number of indexed child variables. - The client can use this optional information to present the children in a paged UI and fetch them in chunks. - */ - indexedVariables?: number; - /** Optional memory reference for the variable if the variable represents executable code, such as a function pointer. */ - memoryReference?: string; - } - - /** Optional properties of a variable that can be used to determine how to render the variable in the UI. */ - export interface VariablePresentationHint { - /** The kind of variable. Before introducing additional values, try to use the listed values. - Values: - 'property': Indicates that the object is a property. - 'method': Indicates that the object is a method. - 'class': Indicates that the object is a class. - 'data': Indicates that the object is data. - 'event': Indicates that the object is an event. - 'baseClass': Indicates that the object is a base class. - 'innerClass': Indicates that the object is an inner class. - 'interface': Indicates that the object is an interface. - 'mostDerivedClass': Indicates that the object is the most derived class. - 'virtual': Indicates that the object is virtual, that means it is a synthetic object introduced by the adapter for rendering purposes, e.g. an index range for large arrays. - 'dataBreakpoint': Indicates that a data breakpoint is registered for the object. - etc. - */ - kind?: string; - /** Set of attributes represented as an array of strings. Before introducing additional values, try to use the listed values. - Values: - 'static': Indicates that the object is static. - 'constant': Indicates that the object is a constant. - 'readOnly': Indicates that the object is read only. - 'rawString': Indicates that the object is a raw string. - 'hasObjectId': Indicates that the object can have an Object ID created for it. - 'canHaveObjectId': Indicates that the object has an Object ID associated with it. - 'hasSideEffects': Indicates that the evaluation had side effects. - etc. - */ - attributes?: string[]; - /** Visibility of variable. Before introducing additional values, try to use the listed values. - Values: 'public', 'private', 'protected', 'internal', 'final', etc. - */ - visibility?: string; - } - - /** Properties of a breakpoint location returned from the 'breakpointLocations' request. */ - export interface BreakpointLocation { - /** Start line of breakpoint location. */ - line: number; - /** Optional start column of breakpoint location. */ - column?: number; - /** Optional end line of breakpoint location if the location covers a range. */ - endLine?: number; - /** Optional end column of breakpoint location if the location covers a range. */ - endColumn?: number; - } - - /** Properties of a breakpoint or logpoint passed to the setBreakpoints request. */ - export interface SourceBreakpoint { - /** The source line of the breakpoint or logpoint. */ - line: number; - /** An optional source column of the breakpoint. */ - column?: number; - /** An optional expression for conditional breakpoints. */ - condition?: string; - /** An optional expression that controls how many hits of the breakpoint are ignored. The backend is expected to interpret the expression as needed. */ - hitCondition?: string; - /** If this attribute exists and is non-empty, the backend must not 'break' (stop) but log the message instead. Expressions within {} are interpolated. */ - logMessage?: string; - } - - /** Properties of a breakpoint passed to the setFunctionBreakpoints request. */ - export interface FunctionBreakpoint { - /** The name of the function. */ - name: string; - /** An optional expression for conditional breakpoints. */ - condition?: string; - /** An optional expression that controls how many hits of the breakpoint are ignored. The backend is expected to interpret the expression as needed. */ - hitCondition?: string; - } - - /** This enumeration defines all possible access types for data breakpoints. */ - export type DataBreakpointAccessType = 'read' | 'write' | 'readWrite'; - - /** Properties of a data breakpoint passed to the setDataBreakpoints request. */ - export interface DataBreakpoint { - /** An id representing the data. This id is returned from the dataBreakpointInfo request. */ - dataId: string; - /** The access type of the data. */ - accessType?: DataBreakpointAccessType; - /** An optional expression for conditional breakpoints. */ - condition?: string; - /** An optional expression that controls how many hits of the breakpoint are ignored. The backend is expected to interpret the expression as needed. */ - hitCondition?: string; - } - - /** Information about a Breakpoint created in setBreakpoints or setFunctionBreakpoints. */ - export interface Breakpoint { - /** An optional identifier for the breakpoint. It is needed if breakpoint events are used to update or remove breakpoints. */ - id?: number; - /** If true breakpoint could be set (but not necessarily at the desired location). */ - verified: boolean; - /** An optional message about the state of the breakpoint. This is shown to the user and can be used to explain why a breakpoint could not be verified. */ - message?: string; - /** The source where the breakpoint is located. */ - source?: Source; - /** The start line of the actual range covered by the breakpoint. */ - line?: number; - /** An optional start column of the actual range covered by the breakpoint. */ - column?: number; - /** An optional end line of the actual range covered by the breakpoint. */ - endLine?: number; - /** An optional end column of the actual range covered by the breakpoint. If no end line is given, then the end column is assumed to be in the start line. */ - endColumn?: number; - } - - /** A StepInTarget can be used in the 'stepIn' request and determines into which single target the stepIn request should step. */ - export interface StepInTarget { - /** Unique identifier for a stepIn target. */ - id: number; - /** The name of the stepIn target (shown in the UI). */ - label: string; - } - - /** A GotoTarget describes a code location that can be used as a target in the 'goto' request. - The possible goto targets can be determined via the 'gotoTargets' request. - */ - export interface GotoTarget { - /** Unique identifier for a goto target. This is used in the goto request. */ - id: number; - /** The name of the goto target (shown in the UI). */ - label: string; - /** The line of the goto target. */ - line: number; - /** An optional column of the goto target. */ - column?: number; - /** An optional end line of the range covered by the goto target. */ - endLine?: number; - /** An optional end column of the range covered by the goto target. */ - endColumn?: number; - /** Optional memory reference for the instruction pointer value represented by this target. */ - instructionPointerReference?: string; - } - - /** CompletionItems are the suggestions returned from the CompletionsRequest. */ - export interface CompletionItem { - /** The label of this completion item. By default this is also the text that is inserted when selecting this completion. */ - label: string; - /** If text is not falsy then it is inserted instead of the label. */ - text?: string; - /** A string that should be used when comparing this item with other items. When `falsy` the label is used. */ - sortText?: string; - /** The item's type. Typically the client uses this information to render the item in the UI with an icon. */ - type?: CompletionItemType; - /** This value determines the location (in the CompletionsRequest's 'text' attribute) where the completion text is added. - If missing the text is added at the location specified by the CompletionsRequest's 'column' attribute. - */ - start?: number; - /** This value determines how many characters are overwritten by the completion text. - If missing the value 0 is assumed which results in the completion text being inserted. - */ - length?: number; - } - - /** Some predefined types for the CompletionItem. Please note that not all clients have specific icons for all of them. */ - export type CompletionItemType = 'method' | 'function' | 'constructor' | 'field' | 'variable' | 'class' | 'interface' | 'module' | 'property' | 'unit' | 'value' | 'enum' | 'keyword' | 'snippet' | 'text' | 'color' | 'file' | 'reference' | 'customcolor'; - - /** Names of checksum algorithms that may be supported by a debug adapter. */ - export type ChecksumAlgorithm = 'MD5' | 'SHA1' | 'SHA256' | 'timestamp'; - - /** The checksum of an item calculated by the specified algorithm. */ - export interface Checksum { - /** The algorithm used to calculate this checksum. */ - algorithm: ChecksumAlgorithm; - /** Value of the checksum. */ - checksum: string; - } - - /** Provides formatting information for a value. */ - export interface ValueFormat { - /** Display the value in hex. */ - hex?: boolean; - } - - /** Provides formatting information for a stack frame. */ - export interface StackFrameFormat extends ValueFormat { - /** Displays parameters for the stack frame. */ - parameters?: boolean; - /** Displays the types of parameters for the stack frame. */ - parameterTypes?: boolean; - /** Displays the names of parameters for the stack frame. */ - parameterNames?: boolean; - /** Displays the values of parameters for the stack frame. */ - parameterValues?: boolean; - /** Displays the line number of the stack frame. */ - line?: boolean; - /** Displays the module of the stack frame. */ - module?: boolean; - /** Includes all stack frames, including those the debug adapter might otherwise hide. */ - includeAll?: boolean; - } - - /** An ExceptionOptions assigns configuration options to a set of exceptions. */ - export interface ExceptionOptions { - /** A path that selects a single or multiple exceptions in a tree. If 'path' is missing, the whole tree is selected. By convention the first segment of the path is a category that is used to group exceptions in the UI. */ - path?: ExceptionPathSegment[]; - /** Condition when a thrown exception should result in a break. */ - breakMode: ExceptionBreakMode; - } - - /** This enumeration defines all possible conditions when a thrown exception should result in a break. - never: never breaks, - always: always breaks, - unhandled: breaks when exception unhandled, - userUnhandled: breaks if the exception is not handled by user code. - */ - export type ExceptionBreakMode = 'never' | 'always' | 'unhandled' | 'userUnhandled'; - - /** An ExceptionPathSegment represents a segment in a path that is used to match leafs or nodes in a tree of exceptions. If a segment consists of more than one name, it matches the names provided if 'negate' is false or missing or it matches anything except the names provided if 'negate' is true. */ - export interface ExceptionPathSegment { - /** If false or missing this segment matches the names provided, otherwise it matches anything except the names provided. */ - negate?: boolean; - /** Depending on the value of 'negate' the names that should match or not match. */ - names: string[]; - } - - /** Detailed information about an exception that has occurred. */ - export interface ExceptionDetails { - /** Message contained in the exception. */ - message?: string; - /** Short type name of the exception object. */ - typeName?: string; - /** Fully-qualified type name of the exception object. */ - fullTypeName?: string; - /** Optional expression that can be evaluated in the current scope to obtain the exception object. */ - evaluateName?: string; - /** Stack trace at the time the exception was thrown. */ - stackTrace?: string; - /** Details of the exception contained by this exception, if any. */ - innerException?: ExceptionDetails[]; - } - - /** Represents a single disassembled instruction. */ - export interface DisassembledInstruction { - /** The address of the instruction. Treated as a hex value if prefixed with '0x', or as a decimal value otherwise. */ - address: string; - /** Optional raw bytes representing the instruction and its operands, in an implementation-defined format. */ - instructionBytes?: string; - /** Text representing the instruction and its operands, in an implementation-defined format. */ - instruction: string; - /** Name of the symbol that corresponds with the location of this instruction, if any. */ - symbol?: string; - /** Source location that corresponds to this instruction, if any. Should always be set (if available) on the first instruction returned, but can be omitted afterwards if this instruction maps to the same source file as the previous instruction. */ - location?: Source; - /** The line within the source location that corresponds to this instruction, if any. */ - line?: number; - /** The column within the line that corresponds to this instruction, if any. */ - column?: number; - /** The end line of the range that corresponds to this instruction, if any. */ - endLine?: number; - /** The end column of the range that corresponds to this instruction, if any. */ - endColumn?: number; - } -} - -//------------------------------------------------------------------------------------------------------------------------------ - -export class Message implements DebugProtocol.ProtocolMessage { - seq: number; - type: string; - - public constructor(type: string) { - this.seq = 0; - this.type = type; - } -} - -export class Response extends Message implements DebugProtocol.Response { - request_seq: number; - success: boolean; - command: string; - - public constructor(request: DebugProtocol.Request, message?: string) { - super('response'); - this.request_seq = request.seq; - this.command = request.command; - if (message) { - this.success = false; - (this).message = message; - } else { - this.success = true; - } - } -} - -export class Event extends Message implements DebugProtocol.Event { - event: string; - - public constructor(event: string, body?: any) { - super('event'); - this.event = event; - if (body) { - (this).body = body; - } - } -} - -//-------------------------------------------------------------------------------------------------------------------------------- - -export class ProtocolServer implements vscode.DebugAdapter { - - private close = new vscode.EventEmitter(); - onClose: vscode.Event = this.close.event; - - private error = new vscode.EventEmitter(); - onError: vscode.Event = this.error.event; - - private sendMessage = new vscode.EventEmitter(); - readonly onDidSendMessage: vscode.Event = this.sendMessage.event; - - private _sequence: number = 1; - private _pendingRequests = new Map void>(); - - - public handleMessage(message: DebugProtocol.ProtocolMessage): void { - this.dispatch(message); - } - - public dispose() { - } - - public sendEvent(event: DebugProtocol.Event): void { - this._send('event', event); - } - - public sendResponse(response: DebugProtocol.Response): void { - if (response.seq > 0) { - console.error(`attempt to send more than one response for command ${response.command}`); - } else { - this._send('response', response); - } - } - - public sendRequest(command: string, args: any, timeout: number, cb: (response: DebugProtocol.Response) => void): void { - - const request: any = { - command: command - }; - if (args && Object.keys(args).length > 0) { - request.arguments = args; - } - - this._send('request', request); - - if (cb) { - this._pendingRequests.set(request.seq, cb); - - const timer = setTimeout(() => { - clearTimeout(timer); - const clb = this._pendingRequests.get(request.seq); - if (clb) { - this._pendingRequests.delete(request.seq); - clb(new Response(request, 'timeout')); - } - }, timeout); - } - } - - // ---- protected ---------------------------------------------------------- - - protected dispatchRequest(_request: DebugProtocol.Request): void { - } - - // ---- private ------------------------------------------------------------ - - private dispatch(msg: DebugProtocol.ProtocolMessage) { - if (msg.type === 'request') { - this.dispatchRequest(msg); - } else if (msg.type === 'response') { - const response = msg; - const clb = this._pendingRequests.get(response.request_seq); - if (clb) { - this._pendingRequests.delete(response.request_seq); - clb(response); - } - } - } - - private _send(typ: 'request' | 'response' | 'event', message: DebugProtocol.ProtocolMessage): void { - - message.type = typ; - message.seq = this._sequence++; - - this.sendMessage.fire(message); - } -} - -//------------------------------------------------------------------------------------------------------------------------------- - -export class Source implements DebugProtocol.Source { - name: string; - path?: string; - sourceReference: number; - - public constructor(name: string, path?: string, id: number = 0, origin?: string, data?: any) { - this.name = name; - this.path = path; - this.sourceReference = id; - if (origin) { - (this).origin = origin; - } - if (data) { - (this).adapterData = data; - } - } -} - -export class Scope implements DebugProtocol.Scope { - name: string; - variablesReference: number; - expensive: boolean; - - public constructor(name: string, reference: number, expensive: boolean = false) { - this.name = name; - this.variablesReference = reference; - this.expensive = expensive; - } -} - -export class StackFrame implements DebugProtocol.StackFrame { - id: number; - source?: Source; - line: number; - column: number; - name: string; - - public constructor(i: number, nm: string, src?: Source, ln: number = 0, col: number = 0) { - this.id = i; - this.source = src; - this.line = ln; - this.column = col; - this.name = nm; - } -} - -export class Thread implements DebugProtocol.Thread { - id: number; - name: string; - - public constructor(id: number, name: string) { - this.id = id; - if (name) { - this.name = name; - } else { - this.name = 'Thread #' + id; - } - } -} - -export class Variable implements DebugProtocol.Variable { - name: string; - value: string; - variablesReference: number; - - public constructor(name: string, value: string, ref: number = 0, indexedVariables?: number, namedVariables?: number) { - this.name = name; - this.value = value; - this.variablesReference = ref; - if (typeof namedVariables === 'number') { - (this).namedVariables = namedVariables; - } - if (typeof indexedVariables === 'number') { - (this).indexedVariables = indexedVariables; - } - } -} - -export class Breakpoint implements DebugProtocol.Breakpoint { - verified: boolean; - - public constructor(verified: boolean, line?: number, column?: number, source?: Source) { - this.verified = verified; - const e: DebugProtocol.Breakpoint = this; - if (typeof line === 'number') { - e.line = line; - } - if (typeof column === 'number') { - e.column = column; - } - if (source) { - e.source = source; - } - } -} - -export class Module implements DebugProtocol.Module { - id: number | string; - name: string; - - public constructor(id: number | string, name: string) { - this.id = id; - this.name = name; - } -} - -export class CompletionItem implements DebugProtocol.CompletionItem { - label: string; - start: number; - length: number; - - public constructor(label: string, start: number, length: number = 0) { - this.label = label; - this.start = start; - this.length = length; - } -} - -export class StoppedEvent extends Event implements DebugProtocol.StoppedEvent { - body: { - reason: string; - }; - - public constructor(reason: string, threadId?: number, exceptionText?: string) { - super('stopped'); - this.body = { - reason: reason - }; - if (typeof threadId === 'number') { - (this as DebugProtocol.StoppedEvent).body.threadId = threadId; - } - if (typeof exceptionText === 'string') { - (this as DebugProtocol.StoppedEvent).body.text = exceptionText; - } - } -} - -export class ContinuedEvent extends Event implements DebugProtocol.ContinuedEvent { - body: { - threadId: number; - }; - - public constructor(threadId: number, allThreadsContinued?: boolean) { - super('continued'); - this.body = { - threadId: threadId - }; - - if (typeof allThreadsContinued === 'boolean') { - (this).body.allThreadsContinued = allThreadsContinued; - } - } -} - -export class InitializedEvent extends Event implements DebugProtocol.InitializedEvent { - public constructor() { - super('initialized'); - } -} - -export class TerminatedEvent extends Event implements DebugProtocol.TerminatedEvent { - public constructor(restart?: any) { - super('terminated'); - if (typeof restart === 'boolean' || restart) { - const e: DebugProtocol.TerminatedEvent = this; - e.body = { - restart: restart - }; - } - } -} - -export class OutputEvent extends Event implements DebugProtocol.OutputEvent { - body: { - category: string, - output: string, - data?: any - }; - - public constructor(output: string, category: string = 'console', data?: any) { - super('output'); - this.body = { - category: category, - output: output - }; - if (data !== undefined) { - this.body.data = data; - } - } -} - -export class ThreadEvent extends Event implements DebugProtocol.ThreadEvent { - body: { - reason: string, - threadId: number - }; - - public constructor(reason: string, threadId: number) { - super('thread'); - this.body = { - reason: reason, - threadId: threadId - }; - } -} - -export class BreakpointEvent extends Event implements DebugProtocol.BreakpointEvent { - body: { - reason: string, - breakpoint: Breakpoint - }; - - public constructor(reason: string, breakpoint: Breakpoint) { - super('breakpoint'); - this.body = { - reason: reason, - breakpoint: breakpoint - }; - } -} - -export class ModuleEvent extends Event implements DebugProtocol.ModuleEvent { - body: { - reason: 'new' | 'changed' | 'removed', - module: Module - }; - - public constructor(reason: 'new' | 'changed' | 'removed', module: Module) { - super('module'); - this.body = { - reason: reason, - module: module - }; - } -} - -export class LoadedSourceEvent extends Event implements DebugProtocol.LoadedSourceEvent { - body: { - reason: 'new' | 'changed' | 'removed', - source: Source - }; - - public constructor(reason: 'new' | 'changed' | 'removed', source: Source) { - super('loadedSource'); - this.body = { - reason: reason, - source: source - }; - } -} - -export class CapabilitiesEvent extends Event implements DebugProtocol.CapabilitiesEvent { - body: { - capabilities: DebugProtocol.Capabilities - }; - - public constructor(capabilities: DebugProtocol.Capabilities) { - super('capabilities'); - this.body = { - capabilities: capabilities - }; - } -} - -export enum ErrorDestination { - User = 1, - Telemetry = 2 -} - -export class DebugSession extends ProtocolServer { - - private _debuggerLinesStartAt1: boolean; - private _debuggerColumnsStartAt1: boolean; - private _debuggerPathsAreURIs: boolean; - - private _clientLinesStartAt1: boolean; - private _clientColumnsStartAt1: boolean; - private _clientPathsAreURIs: boolean; - - protected _isServer: boolean; - - public constructor(obsolete_debuggerLinesAndColumnsStartAt1?: boolean, obsolete_isServer?: boolean) { - super(); - - const linesAndColumnsStartAt1 = typeof obsolete_debuggerLinesAndColumnsStartAt1 === 'boolean' ? obsolete_debuggerLinesAndColumnsStartAt1 : false; - this._debuggerLinesStartAt1 = linesAndColumnsStartAt1; - this._debuggerColumnsStartAt1 = linesAndColumnsStartAt1; - this._debuggerPathsAreURIs = false; - - this._clientLinesStartAt1 = true; - this._clientColumnsStartAt1 = true; - this._clientPathsAreURIs = false; - - this._isServer = typeof obsolete_isServer === 'boolean' ? obsolete_isServer : false; - - this.onClose(() => { - this.shutdown(); - }); - this.onError((_error) => { - this.shutdown(); - }); - } - - public setDebuggerPathFormat(format: string) { - this._debuggerPathsAreURIs = format !== 'path'; - } - - public setDebuggerLinesStartAt1(enable: boolean) { - this._debuggerLinesStartAt1 = enable; - } - - public setDebuggerColumnsStartAt1(enable: boolean) { - this._debuggerColumnsStartAt1 = enable; - } - - public setRunAsServer(enable: boolean) { - this._isServer = enable; - } - - public shutdown(): void { - if (this._isServer) { - // shutdown ignored in server mode - } else { - // TODO@AW - /* - // wait a bit before shutting down - setTimeout(() => { - process.exit(0); - }, 100); - */ - } - } - - protected sendErrorResponse(response: DebugProtocol.Response, codeOrMessage: number | DebugProtocol.Message, format?: string, variables?: any, dest: ErrorDestination = ErrorDestination.User): void { - - let msg: DebugProtocol.Message; - if (typeof codeOrMessage === 'number') { - msg = { - id: codeOrMessage, - format: format - }; - if (variables) { - msg.variables = variables; - } - if (dest & ErrorDestination.User) { - msg.showUser = true; - } - if (dest & ErrorDestination.Telemetry) { - msg.sendTelemetry = true; - } - } else { - msg = codeOrMessage; - } - - response.success = false; - response.message = DebugSession.formatPII(msg.format, true, msg.variables); - if (!response.body) { - response.body = {}; - } - response.body.error = msg; - - this.sendResponse(response); - } - - public runInTerminalRequest(args: DebugProtocol.RunInTerminalRequestArguments, timeout: number, cb: (response: DebugProtocol.Response) => void) { - this.sendRequest('runInTerminal', args, timeout, cb); - } - - protected dispatchRequest(request: DebugProtocol.Request): void { - - const response = new Response(request); - - try { - if (request.command === 'initialize') { - const args = request.arguments; - - if (typeof args.linesStartAt1 === 'boolean') { - this._clientLinesStartAt1 = args.linesStartAt1; - } - if (typeof args.columnsStartAt1 === 'boolean') { - this._clientColumnsStartAt1 = args.columnsStartAt1; - } - - if (args.pathFormat !== 'path') { - this.sendErrorResponse(response, 2018, 'debug adapter only supports native paths', null, ErrorDestination.Telemetry); - } else { - const initializeResponse = response; - initializeResponse.body = {}; - this.initializeRequest(initializeResponse, args); - } - - } else if (request.command === 'launch') { - this.launchRequest(response, request.arguments, request); - - } else if (request.command === 'attach') { - this.attachRequest(response, request.arguments, request); - - } else if (request.command === 'disconnect') { - this.disconnectRequest(response, request.arguments, request); - - } else if (request.command === 'terminate') { - this.terminateRequest(response, request.arguments, request); - - } else if (request.command === 'restart') { - this.restartRequest(response, request.arguments, request); - - } else if (request.command === 'setBreakpoints') { - this.setBreakPointsRequest(response, request.arguments, request); - - } else if (request.command === 'setFunctionBreakpoints') { - this.setFunctionBreakPointsRequest(response, request.arguments, request); - - } else if (request.command === 'setExceptionBreakpoints') { - this.setExceptionBreakPointsRequest(response, request.arguments, request); - - } else if (request.command === 'configurationDone') { - this.configurationDoneRequest(response, request.arguments, request); - - } else if (request.command === 'continue') { - this.continueRequest(response, request.arguments, request); - - } else if (request.command === 'next') { - this.nextRequest(response, request.arguments, request); - - } else if (request.command === 'stepIn') { - this.stepInRequest(response, request.arguments, request); - - } else if (request.command === 'stepOut') { - this.stepOutRequest(response, request.arguments, request); - - } else if (request.command === 'stepBack') { - this.stepBackRequest(response, request.arguments, request); - - } else if (request.command === 'reverseContinue') { - this.reverseContinueRequest(response, request.arguments, request); - - } else if (request.command === 'restartFrame') { - this.restartFrameRequest(response, request.arguments, request); - - } else if (request.command === 'goto') { - this.gotoRequest(response, request.arguments, request); - - } else if (request.command === 'pause') { - this.pauseRequest(response, request.arguments, request); - - } else if (request.command === 'stackTrace') { - this.stackTraceRequest(response, request.arguments, request); - - } else if (request.command === 'scopes') { - this.scopesRequest(response, request.arguments, request); - - } else if (request.command === 'variables') { - this.variablesRequest(response, request.arguments, request); - - } else if (request.command === 'setVariable') { - this.setVariableRequest(response, request.arguments, request); - - } else if (request.command === 'setExpression') { - this.setExpressionRequest(response, request.arguments, request); - - } else if (request.command === 'source') { - this.sourceRequest(response, request.arguments, request); - - } else if (request.command === 'threads') { - this.threadsRequest(response, request); - - } else if (request.command === 'terminateThreads') { - this.terminateThreadsRequest(response, request.arguments, request); - - } else if (request.command === 'evaluate') { - this.evaluateRequest(response, request.arguments, request); - - } else if (request.command === 'stepInTargets') { - this.stepInTargetsRequest(response, request.arguments, request); - - } else if (request.command === 'gotoTargets') { - this.gotoTargetsRequest(response, request.arguments, request); - - } else if (request.command === 'completions') { - this.completionsRequest(response, request.arguments, request); - - } else if (request.command === 'exceptionInfo') { - this.exceptionInfoRequest(response, request.arguments, request); - - } else if (request.command === 'loadedSources') { - this.loadedSourcesRequest(response, request.arguments, request); - - } else if (request.command === 'dataBreakpointInfo') { - this.dataBreakpointInfoRequest(response, request.arguments, request); - - } else if (request.command === 'setDataBreakpoints') { - this.setDataBreakpointsRequest(response, request.arguments, request); - - } else if (request.command === 'readMemory') { - this.readMemoryRequest(response, request.arguments, request); - - } else if (request.command === 'disassemble') { - this.disassembleRequest(response, request.arguments, request); - - } else if (request.command === 'cancel') { - this.cancelRequest(response, request.arguments, request); - - } else if (request.command === 'breakpointLocations') { - this.breakpointLocationsRequest(response, request.arguments, request); - - } else { - this.customRequest(request.command, response, request.arguments, request); - } - } catch (e) { - this.sendErrorResponse(response, 1104, '{_stack}', { _exception: e.message, _stack: e.stack }, ErrorDestination.Telemetry); - } - } - - protected initializeRequest(response: DebugProtocol.InitializeResponse, _args: DebugProtocol.InitializeRequestArguments): void { - - response.body = response.body || {}; - - // This default debug adapter does not support conditional breakpoints. - response.body.supportsConditionalBreakpoints = false; - - // This default debug adapter does not support hit conditional breakpoints. - response.body.supportsHitConditionalBreakpoints = false; - - // This default debug adapter does not support function breakpoints. - response.body.supportsFunctionBreakpoints = false; - - // This default debug adapter implements the 'configurationDone' request. - response.body.supportsConfigurationDoneRequest = true; - - // This default debug adapter does not support hovers based on the 'evaluate' request. - response.body.supportsEvaluateForHovers = false; - - // This default debug adapter does not support the 'stepBack' request. - response.body.supportsStepBack = false; - - // This default debug adapter does not support the 'setVariable' request. - response.body.supportsSetVariable = false; - - // This default debug adapter does not support the 'restartFrame' request. - response.body.supportsRestartFrame = false; - - // This default debug adapter does not support the 'stepInTargets' request. - response.body.supportsStepInTargetsRequest = false; - - // This default debug adapter does not support the 'gotoTargets' request. - response.body.supportsGotoTargetsRequest = false; - - // This default debug adapter does not support the 'completions' request. - response.body.supportsCompletionsRequest = false; - - // This default debug adapter does not support the 'restart' request. - response.body.supportsRestartRequest = false; - - // This default debug adapter does not support the 'exceptionOptions' attribute on the 'setExceptionBreakpoints' request. - response.body.supportsExceptionOptions = false; - - // This default debug adapter does not support the 'format' attribute on the 'variables', 'evaluate', and 'stackTrace' request. - response.body.supportsValueFormattingOptions = false; - - // This debug adapter does not support the 'exceptionInfo' request. - response.body.supportsExceptionInfoRequest = false; - - // This debug adapter does not support the 'TerminateDebuggee' attribute on the 'disconnect' request. - response.body.supportTerminateDebuggee = false; - - // This debug adapter does not support delayed loading of stack frames. - response.body.supportsDelayedStackTraceLoading = false; - - // This debug adapter does not support the 'loadedSources' request. - response.body.supportsLoadedSourcesRequest = false; - - // This debug adapter does not support the 'logMessage' attribute of the SourceBreakpoint. - response.body.supportsLogPoints = false; - - // This debug adapter does not support the 'terminateThreads' request. - response.body.supportsTerminateThreadsRequest = false; - - // This debug adapter does not support the 'setExpression' request. - response.body.supportsSetExpression = false; - - // This debug adapter does not support the 'terminate' request. - response.body.supportsTerminateRequest = false; - - // This debug adapter does not support data breakpoints. - response.body.supportsDataBreakpoints = false; - - /** This debug adapter does not support the 'readMemory' request. */ - response.body.supportsReadMemoryRequest = false; - - /** The debug adapter does not support the 'disassemble' request. */ - response.body.supportsDisassembleRequest = false; - - /** The debug adapter does not support the 'cancel' request. */ - response.body.supportsCancelRequest = false; - - /** The debug adapter does not support the 'breakpointLocations' request. */ - response.body.supportsBreakpointLocationsRequest = false; - - this.sendResponse(response); - } - - protected disconnectRequest(response: DebugProtocol.DisconnectResponse, _args: DebugProtocol.DisconnectArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - this.shutdown(); - } - - protected launchRequest(response: DebugProtocol.LaunchResponse, _args: DebugProtocol.LaunchRequestArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected attachRequest(response: DebugProtocol.AttachResponse, _args: DebugProtocol.AttachRequestArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected terminateRequest(response: DebugProtocol.TerminateResponse, _args: DebugProtocol.TerminateArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected restartRequest(response: DebugProtocol.RestartResponse, _args: DebugProtocol.RestartArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, _args: DebugProtocol.SetBreakpointsArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected setFunctionBreakPointsRequest(response: DebugProtocol.SetFunctionBreakpointsResponse, _args: DebugProtocol.SetFunctionBreakpointsArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected setExceptionBreakPointsRequest(response: DebugProtocol.SetExceptionBreakpointsResponse, _args: DebugProtocol.SetExceptionBreakpointsArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, _args: DebugProtocol.ConfigurationDoneArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected continueRequest(response: DebugProtocol.ContinueResponse, _args: DebugProtocol.ContinueArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected nextRequest(response: DebugProtocol.NextResponse, _args: DebugProtocol.NextArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected stepInRequest(response: DebugProtocol.StepInResponse, _args: DebugProtocol.StepInArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected stepOutRequest(response: DebugProtocol.StepOutResponse, _args: DebugProtocol.StepOutArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected stepBackRequest(response: DebugProtocol.StepBackResponse, _args: DebugProtocol.StepBackArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected reverseContinueRequest(response: DebugProtocol.ReverseContinueResponse, _args: DebugProtocol.ReverseContinueArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected restartFrameRequest(response: DebugProtocol.RestartFrameResponse, _args: DebugProtocol.RestartFrameArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected gotoRequest(response: DebugProtocol.GotoResponse, _args: DebugProtocol.GotoArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected pauseRequest(response: DebugProtocol.PauseResponse, _args: DebugProtocol.PauseArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected sourceRequest(response: DebugProtocol.SourceResponse, _args: DebugProtocol.SourceArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected threadsRequest(response: DebugProtocol.ThreadsResponse, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected terminateThreadsRequest(response: DebugProtocol.TerminateThreadsResponse, _args: DebugProtocol.TerminateThreadsArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, _args: DebugProtocol.StackTraceArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected scopesRequest(response: DebugProtocol.ScopesResponse, _args: DebugProtocol.ScopesArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected variablesRequest(response: DebugProtocol.VariablesResponse, _args: DebugProtocol.VariablesArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected setVariableRequest(response: DebugProtocol.SetVariableResponse, _args: DebugProtocol.SetVariableArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected setExpressionRequest(response: DebugProtocol.SetExpressionResponse, _args: DebugProtocol.SetExpressionArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected evaluateRequest(response: DebugProtocol.EvaluateResponse, _args: DebugProtocol.EvaluateArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected stepInTargetsRequest(response: DebugProtocol.StepInTargetsResponse, _args: DebugProtocol.StepInTargetsArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected gotoTargetsRequest(response: DebugProtocol.GotoTargetsResponse, _args: DebugProtocol.GotoTargetsArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected completionsRequest(response: DebugProtocol.CompletionsResponse, _args: DebugProtocol.CompletionsArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected exceptionInfoRequest(response: DebugProtocol.ExceptionInfoResponse, _args: DebugProtocol.ExceptionInfoArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected loadedSourcesRequest(response: DebugProtocol.LoadedSourcesResponse, _args: DebugProtocol.LoadedSourcesArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected dataBreakpointInfoRequest(response: DebugProtocol.DataBreakpointInfoResponse, _args: DebugProtocol.DataBreakpointInfoArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected setDataBreakpointsRequest(response: DebugProtocol.SetDataBreakpointsResponse, _args: DebugProtocol.SetDataBreakpointsArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected readMemoryRequest(response: DebugProtocol.ReadMemoryResponse, _args: DebugProtocol.ReadMemoryArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected disassembleRequest(response: DebugProtocol.DisassembleResponse, _args: DebugProtocol.DisassembleArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected cancelRequest(response: DebugProtocol.CancelResponse, _args: DebugProtocol.CancelArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected breakpointLocationsRequest(response: DebugProtocol.BreakpointLocationsResponse, _args: DebugProtocol.BreakpointLocationsArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - /** - * Override this hook to implement custom requests. - */ - protected customRequest(_command: string, response: DebugProtocol.Response, _args: any, _request?: DebugProtocol.Request): void { - this.sendErrorResponse(response, 1014, 'unrecognized request', null, ErrorDestination.Telemetry); - } - - //---- protected ------------------------------------------------------------------------------------------------- - - protected convertClientLineToDebugger(line: number): number { - if (this._debuggerLinesStartAt1) { - return this._clientLinesStartAt1 ? line : line + 1; - } - return this._clientLinesStartAt1 ? line - 1 : line; - } - - protected convertDebuggerLineToClient(line: number): number { - if (this._debuggerLinesStartAt1) { - return this._clientLinesStartAt1 ? line : line - 1; - } - return this._clientLinesStartAt1 ? line + 1 : line; - } - - protected convertClientColumnToDebugger(column: number): number { - if (this._debuggerColumnsStartAt1) { - return this._clientColumnsStartAt1 ? column : column + 1; - } - return this._clientColumnsStartAt1 ? column - 1 : column; - } - - protected convertDebuggerColumnToClient(column: number): number { - if (this._debuggerColumnsStartAt1) { - return this._clientColumnsStartAt1 ? column : column - 1; - } - return this._clientColumnsStartAt1 ? column + 1 : column; - } - - protected convertClientPathToDebugger(clientPath: string): string { - if (this._clientPathsAreURIs !== this._debuggerPathsAreURIs) { - if (this._clientPathsAreURIs) { - return DebugSession.uri2path(clientPath); - } else { - return DebugSession.path2uri(clientPath); - } - } - return clientPath; - } - - protected convertDebuggerPathToClient(debuggerPath: string): string { - if (this._debuggerPathsAreURIs !== this._clientPathsAreURIs) { - if (this._debuggerPathsAreURIs) { - return DebugSession.uri2path(debuggerPath); - } else { - return DebugSession.path2uri(debuggerPath); - } - } - return debuggerPath; - } - - //---- private ------------------------------------------------------------------------------- - - private static path2uri(path: string): string { - - path = encodeURI(path); - - let uri = new URL(`file:`); // ignore 'path' for now - uri.pathname = path; // now use 'path' to get the correct percent encoding (see https://url.spec.whatwg.org) - return uri.toString(); - } - - private static uri2path(sourceUri: string): string { - - let uri = new URL(sourceUri); - let s = decodeURIComponent(uri.pathname); - return s; - } - - private static _formatPIIRegexp = /{([^}]+)}/g; - - /* - * If argument starts with '_' it is OK to send its value to telemetry. - */ - private static formatPII(format: string, excludePII: boolean, args?: { [key: string]: string }): string { - return format.replace(DebugSession._formatPIIRegexp, function (match, paramName) { - if (excludePII && paramName.length > 0 && paramName[0] !== '_') { - return match; - } - return args && args[paramName] && args.hasOwnProperty(paramName) ? - args[paramName] : - match; - }); - } -} - -//--------------------------------------------------------------------------- - -export class Handles { - - private START_HANDLE = 1000; - - private _nextHandle: number; - private _handleMap = new Map(); - - public constructor(startHandle?: number) { - this._nextHandle = typeof startHandle === 'number' ? startHandle : this.START_HANDLE; - } - - public reset(): void { - this._nextHandle = this.START_HANDLE; - this._handleMap = new Map(); - } - - public create(value: T): number { - const handle = this._nextHandle++; - this._handleMap.set(handle, value); - return handle; - } - - public get(handle: number, dflt?: T): T | undefined { - return this._handleMap.get(handle) || dflt; - } -} - -//--------------------------------------------------------------------------- - -class MockConfigurationProvider implements vscode.DebugConfigurationProvider { - - /** - * Massage a debug configuration just before a debug session is being launched, - * e.g. add all missing attributes to the debug configuration. - */ - resolveDebugConfiguration(_folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, _token?: vscode.CancellationToken): vscode.ProviderResult { - - // if launch.json is missing or empty - if (!config.type && !config.request && !config.name) { - const editor = vscode.window.activeTextEditor; - if (editor && editor.document.languageId === 'markdown') { - config.type = 'mock'; - config.name = 'Launch'; - config.request = 'launch'; - config.program = '${file}'; - config.stopOnEntry = true; - } - } - - if (!config.program) { - return vscode.window.showInformationMessage('Cannot find a program to debug').then(_ => { - return undefined; // abort launch - }); - } - - return config; - } -} - -export class MockDebugAdapterDescriptorFactory implements vscode.DebugAdapterDescriptorFactory { - - constructor(private memfs: MemFS) { - } - - createDebugAdapterDescriptor(_session: vscode.DebugSession, _executable: vscode.DebugAdapterExecutable | undefined): vscode.ProviderResult { - return new vscode.DebugAdapterInlineImplementation(new MockDebugSession(this.memfs)); - } -} - -function basename(path: string): string { - const pos = path.lastIndexOf('/'); - if (pos >= 0) { - return path.substring(pos + 1); - } - return path; -} - -function timeout(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -/** - * This interface describes the mock-debug specific launch attributes - * (which are not part of the Debug Adapter Protocol). - * The schema for these attributes lives in the package.json of the mock-debug extension. - * The interface should always match this schema. - */ -interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { - /** An absolute path to the "program" to debug. */ - program: string; - /** Automatically stop target after launch. If not specified, target does not stop. */ - stopOnEntry?: boolean; - /** enable logging the Debug Adapter Protocol */ - trace?: boolean; -} - -export class MockDebugSession extends DebugSession { - - // we don't support multiple threads, so we can use a hardcoded ID for the default thread - private static THREAD_ID = 1; - - // a Mock runtime (or debugger) - private _runtime: MockRuntime; - - private _variableHandles = new Handles(); - - //private _configurationDone = new Subject(); - - private promiseResolve?: () => void; - private _configurationDone = new Promise((r, _e) => { - this.promiseResolve = r; - setTimeout(r, 1000); - }); - - private _cancelationTokens = new Map(); - private _isLongrunning = new Map(); - - /** - * Creates a new debug adapter that is used for one debug session. - * We configure the default implementation of a debug adapter here. - */ - public constructor(memfs: MemFS) { - - super(); - - // this debugger uses zero-based lines and columns - this.setDebuggerLinesStartAt1(false); - this.setDebuggerColumnsStartAt1(false); - - this._runtime = new MockRuntime(memfs); - - // setup event handlers - this._runtime.onStopOnEntry(() => { - this.sendEvent(new StoppedEvent('entry', MockDebugSession.THREAD_ID)); - }); - this._runtime.onStopOnStep(() => { - this.sendEvent(new StoppedEvent('step', MockDebugSession.THREAD_ID)); - }); - this._runtime.onStopOnBreakpoint(() => { - this.sendEvent(new StoppedEvent('breakpoint', MockDebugSession.THREAD_ID)); - }); - this._runtime.onStopOnDataBreakpoint(() => { - this.sendEvent(new StoppedEvent('data breakpoint', MockDebugSession.THREAD_ID)); - }); - this._runtime.onStopOnException(() => { - this.sendEvent(new StoppedEvent('exception', MockDebugSession.THREAD_ID)); - }); - this._runtime.onBreakpointValidated((bp: MockBreakpoint) => { - this.sendEvent(new BreakpointEvent('changed', { verified: bp.verified, id: bp.id })); - }); - this._runtime.onOutput(oe => { - const e: DebugProtocol.OutputEvent = new OutputEvent(`${oe.text}\n`); - e.body.source = this.createSource(oe.filePath); - e.body.line = this.convertDebuggerLineToClient(oe.line); - e.body.column = this.convertDebuggerColumnToClient(oe.column); - this.sendEvent(e); - }); - this._runtime.onEnd(() => { - this.sendEvent(new TerminatedEvent()); - }); - } - - /** - * The 'initialize' request is the first request called by the frontend - * to interrogate the features the debug adapter provides. - */ - protected initializeRequest(response: DebugProtocol.InitializeResponse, _args: DebugProtocol.InitializeRequestArguments): void { - - // build and return the capabilities of this debug adapter: - response.body = response.body || {}; - - // the adapter implements the configurationDoneRequest. - response.body.supportsConfigurationDoneRequest = true; - - // make VS Code to use 'evaluate' when hovering over source - response.body.supportsEvaluateForHovers = true; - - // make VS Code to show a 'step back' button - response.body.supportsStepBack = true; - - // make VS Code to support data breakpoints - response.body.supportsDataBreakpoints = true; - - // make VS Code to support completion in REPL - response.body.supportsCompletionsRequest = true; - response.body.completionTriggerCharacters = ['.', '[']; - - // make VS Code to send cancelRequests - response.body.supportsCancelRequest = true; - - // make VS Code send the breakpointLocations request - response.body.supportsBreakpointLocationsRequest = true; - - this.sendResponse(response); - - // since this debug adapter can accept configuration requests like 'setBreakpoint' at any time, - // we request them early by sending an 'initializeRequest' to the frontend. - // The frontend will end the configuration sequence by calling 'configurationDone' request. - this.sendEvent(new InitializedEvent()); - } - - /** - * Called at the end of the configuration sequence. - * Indicates that all breakpoints etc. have been sent to the DA and that the 'launch' can start. - */ - protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments): void { - super.configurationDoneRequest(response, args); - - // notify the launchRequest that configuration has finished - //this._configurationDone.notify(); - if (this.promiseResolve) { - this.promiseResolve(); - } - } - - protected async launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments) { - - // make sure to 'Stop' the buffered logging if 'trace' is not set - //logger.setup(args.trace ? Logger.LogLevel.Verbose : Logger.LogLevel.Stop, false); - - // wait until configuration has finished (and configurationDoneRequest has been called) - await this._configurationDone; - - // start the program in the runtime - this._runtime.start(`memfs:${args.program}`, !!args.stopOnEntry); - - this.sendResponse(response); - } - - protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void { - - const path = args.source.path; - const clientLines = args.lines || []; - - // clear all breakpoints for this file - this._runtime.clearBreakpoints(path); - - // set and verify breakpoint locations - const actualBreakpoints = clientLines.map(l => { - let { verified, line, id } = this._runtime.setBreakPoint(path, this.convertClientLineToDebugger(l)); - const bp = new Breakpoint(verified, this.convertDebuggerLineToClient(line)); - bp.id = id; - return bp; - }); - - // send back the actual breakpoint positions - response.body = { - breakpoints: actualBreakpoints - }; - this.sendResponse(response); - } - - protected breakpointLocationsRequest(response: DebugProtocol.BreakpointLocationsResponse, args: DebugProtocol.BreakpointLocationsArguments, _request?: DebugProtocol.Request): void { - - if (args.source.path) { - const bps = this._runtime.getBreakpoints(args.source.path, this.convertClientLineToDebugger(args.line)); - response.body = { - breakpoints: bps.map(col => { - return { - line: args.line, - column: this.convertDebuggerColumnToClient(col) - }; - }) - }; - } else { - response.body = { - breakpoints: [] - }; - } - this.sendResponse(response); - } - - protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { - - // runtime supports no threads so just return a default thread. - response.body = { - threads: [ - new Thread(MockDebugSession.THREAD_ID, 'thread 1') - ] - }; - this.sendResponse(response); - } - - protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void { - - const startFrame = typeof args.startFrame === 'number' ? args.startFrame : 0; - const maxLevels = typeof args.levels === 'number' ? args.levels : 1000; - const endFrame = startFrame + maxLevels; - - const stk = this._runtime.stack(startFrame, endFrame); - - response.body = { - stackFrames: stk.frames.map(f => new StackFrame(f.index, f.name, this.createSource(f.file), this.convertDebuggerLineToClient(f.line))), - totalFrames: stk.count - }; - this.sendResponse(response); - } - - protected scopesRequest(response: DebugProtocol.ScopesResponse, _args: DebugProtocol.ScopesArguments): void { - - response.body = { - scopes: [ - new Scope('Local', this._variableHandles.create('local'), false), - new Scope('Global', this._variableHandles.create('global'), true) - ] - }; - this.sendResponse(response); - } - - protected async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments, request?: DebugProtocol.Request) { - - const variables: DebugProtocol.Variable[] = []; - - if (this._isLongrunning.get(args.variablesReference)) { - // long running - - if (request) { - this._cancelationTokens.set(request.seq, false); - } - - for (let i = 0; i < 100; i++) { - await timeout(1000); - variables.push({ - name: `i_${i}`, - type: 'integer', - value: `${i}`, - variablesReference: 0 - }); - if (request && this._cancelationTokens.get(request.seq)) { - break; - } - } - - if (request) { - this._cancelationTokens.delete(request.seq); - } - - } else { - - const id = this._variableHandles.get(args.variablesReference); - - if (id) { - variables.push({ - name: id + '_i', - type: 'integer', - value: '123', - variablesReference: 0 - }); - variables.push({ - name: id + '_f', - type: 'float', - value: '3.14', - variablesReference: 0 - }); - variables.push({ - name: id + '_s', - type: 'string', - value: 'hello world', - variablesReference: 0 - }); - variables.push({ - name: id + '_o', - type: 'object', - value: 'Object', - variablesReference: this._variableHandles.create(id + '_o') - }); - - // cancelation support for long running requests - const nm = id + '_long_running'; - const ref = this._variableHandles.create(id + '_lr'); - variables.push({ - name: nm, - type: 'object', - value: 'Object', - variablesReference: ref - }); - this._isLongrunning.set(ref, true); - } - } - - response.body = { - variables: variables - }; - this.sendResponse(response); - } - - protected continueRequest(response: DebugProtocol.ContinueResponse, _args: DebugProtocol.ContinueArguments): void { - this._runtime.continue(); - this.sendResponse(response); - } - - protected reverseContinueRequest(response: DebugProtocol.ReverseContinueResponse, _args: DebugProtocol.ReverseContinueArguments): void { - this._runtime.continue(true); - this.sendResponse(response); - } - - protected nextRequest(response: DebugProtocol.NextResponse, _args: DebugProtocol.NextArguments): void { - this._runtime.step(); - this.sendResponse(response); - } - - protected stepBackRequest(response: DebugProtocol.StepBackResponse, _args: DebugProtocol.StepBackArguments): void { - this._runtime.step(true); - this.sendResponse(response); - } - - protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { - - let reply: string | undefined = undefined; - - if (args.context === 'repl') { - // 'evaluate' supports to create and delete breakpoints from the 'repl': - const matches = /new +([0-9]+)/.exec(args.expression); - if (matches && matches.length === 2) { - if (this._runtime.sourceFile) { - const mbp = this._runtime.setBreakPoint(this._runtime.sourceFile, this.convertClientLineToDebugger(parseInt(matches[1]))); - const bp = new Breakpoint(mbp.verified, this.convertDebuggerLineToClient(mbp.line), undefined, this.createSource(this._runtime.sourceFile)); - bp.id = mbp.id; - this.sendEvent(new BreakpointEvent('new', bp)); - reply = `breakpoint created`; - } - } else { - const matches = /del +([0-9]+)/.exec(args.expression); - if (matches && matches.length === 2) { - const mbp = this._runtime.sourceFile ? this._runtime.clearBreakPoint(this._runtime.sourceFile, this.convertClientLineToDebugger(parseInt(matches[1]))) : undefined; - if (mbp) { - const bp = new Breakpoint(false); - bp.id = mbp.id; - this.sendEvent(new BreakpointEvent('removed', bp)); - reply = `breakpoint deleted`; - } - } - } - } - - response.body = { - result: reply ? reply : `evaluate(context: '${args.context}', '${args.expression}')`, - variablesReference: 0 - }; - this.sendResponse(response); - } - - protected dataBreakpointInfoRequest(response: DebugProtocol.DataBreakpointInfoResponse, args: DebugProtocol.DataBreakpointInfoArguments): void { - - response.body = { - dataId: null, - description: 'cannot break on data access', - accessTypes: undefined, - canPersist: false - }; - - if (args.variablesReference && args.name) { - const id = this._variableHandles.get(args.variablesReference); - if (id && id.startsWith('global_')) { - response.body.dataId = args.name; - response.body.description = args.name; - response.body.accessTypes = ['read']; - response.body.canPersist = false; - } - } - - this.sendResponse(response); - } - - protected setDataBreakpointsRequest(response: DebugProtocol.SetDataBreakpointsResponse, args: DebugProtocol.SetDataBreakpointsArguments): void { - - // clear all data breakpoints - this._runtime.clearAllDataBreakpoints(); - - response.body = { - breakpoints: [] - }; - - for (let dbp of args.breakpoints) { - // assume that id is the "address" to break on - const ok = this._runtime.setDataBreakpoint(dbp.dataId); - response.body.breakpoints.push({ - verified: ok - }); - } - - this.sendResponse(response); - } - - protected completionsRequest(response: DebugProtocol.CompletionsResponse, _args: DebugProtocol.CompletionsArguments): void { - - response.body = { - targets: [ - { - label: 'item 10', - sortText: '10' - }, - { - label: 'item 1', - sortText: '01' - }, - { - label: 'item 2', - sortText: '02' - } - ] - }; - this.sendResponse(response); - } - - protected cancelRequest(_response: DebugProtocol.CancelResponse, args: DebugProtocol.CancelArguments) { - if (args.requestId) { - this._cancelationTokens.set(args.requestId, true); - } - } - - //---- helpers - - private createSource(filePath: string): Source { - return new Source(basename(filePath), this.convertDebuggerPathToClient(filePath), undefined, undefined, 'mock-adapter-data'); - } -} - -//------------------------------------------------------------------------------------------------------------------------------------------ - - -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -export interface MockBreakpoint { - id: number; - line: number; - verified: boolean; -} - -export interface MockOutputEvent { - text: string; - filePath: string; - line: number; - column: number; -} - -/** - * A Mock runtime with minimal debugger functionality. - */ -export class MockRuntime { - - private stopOnEntry = new vscode.EventEmitter(); - onStopOnEntry: vscode.Event = this.stopOnEntry.event; - - private stopOnStep = new vscode.EventEmitter(); - onStopOnStep: vscode.Event = this.stopOnStep.event; - - private stopOnBreakpoint = new vscode.EventEmitter(); - onStopOnBreakpoint: vscode.Event = this.stopOnBreakpoint.event; - - private stopOnDataBreakpoint = new vscode.EventEmitter(); - onStopOnDataBreakpoint: vscode.Event = this.stopOnDataBreakpoint.event; - - private stopOnException = new vscode.EventEmitter(); - onStopOnException: vscode.Event = this.stopOnException.event; - - private breakpointValidated = new vscode.EventEmitter(); - onBreakpointValidated: vscode.Event = this.breakpointValidated.event; - - private output = new vscode.EventEmitter(); - onOutput: vscode.Event = this.output.event; - - private end = new vscode.EventEmitter(); - onEnd: vscode.Event = this.end.event; - - - // the initial (and one and only) file we are 'debugging' - private _sourceFile?: string; - public get sourceFile() { - return this._sourceFile; - } - - // the contents (= lines) of the one and only file - private _sourceLines: string[] = []; - - // This is the next line that will be 'executed' - private _currentLine = 0; - - // maps from sourceFile to array of Mock breakpoints - private _breakPoints = new Map(); - - // since we want to send breakpoint events, we will assign an id to every event - // so that the frontend can match events with breakpoints. - private _breakpointId = 1; - - private _breakAddresses = new Set(); - - constructor(private memfs: MemFS) { - } - - /** - * Start executing the given program. - */ - public start(program: string, stopOnEntry: boolean) { - - this.loadSource(program); - this._currentLine = -1; - - if (this._sourceFile) { - this.verifyBreakpoints(this._sourceFile); - } - - if (stopOnEntry) { - // we step once - this.step(false, this.stopOnEntry); - } else { - // we just start to run until we hit a breakpoint or an exception - this.continue(); - } - } - - /** - * Continue execution to the end/beginning. - */ - public continue(reverse = false) { - this.run(reverse, undefined); - } - - /** - * Step to the next/previous non empty line. - */ - public step(reverse = false, event = this.stopOnStep) { - this.run(reverse, event); - } - - /** - * Returns a fake 'stacktrace' where every 'stackframe' is a word from the current line. - */ - public stack(startFrame: number, endFrame: number): { frames: any[], count: number } { - - const words = this._sourceLines[this._currentLine].trim().split(/\s+/); - - const frames = new Array(); - // every word of the current line becomes a stack frame. - for (let i = startFrame; i < Math.min(endFrame, words.length); i++) { - const name = words[i]; // use a word of the line as the stackframe name - frames.push({ - index: i, - name: `${name}(${i})`, - file: this._sourceFile, - line: this._currentLine - }); - } - return { - frames: frames, - count: words.length - }; - } - - public getBreakpoints(_path: string, line: number): number[] { - - const l = this._sourceLines[line]; - - let sawSpace = true; - const bps: number[] = []; - for (let i = 0; i < l.length; i++) { - if (l[i] !== ' ') { - if (sawSpace) { - bps.push(i); - sawSpace = false; - } - } else { - sawSpace = true; - } - } - - return bps; - } - - /* - * Set breakpoint in file with given line. - */ - public setBreakPoint(path: string, line: number): MockBreakpoint { - - const bp = { verified: false, line, id: this._breakpointId++ }; - let bps = this._breakPoints.get(path); - if (!bps) { - bps = new Array(); - this._breakPoints.set(path, bps); - } - bps.push(bp); - - this.verifyBreakpoints(path); - - return bp; - } - - /* - * Clear breakpoint in file with given line. - */ - public clearBreakPoint(path: string, line: number): MockBreakpoint | undefined { - let bps = this._breakPoints.get(path); - if (bps) { - const index = bps.findIndex(bp => bp.line === line); - if (index >= 0) { - const bp = bps[index]; - bps.splice(index, 1); - return bp; - } - } - return undefined; - } - - /* - * Clear all breakpoints for file. - */ - public clearBreakpoints(path: string): void { - this._breakPoints.delete(path); - } - - /* - * Set data breakpoint. - */ - public setDataBreakpoint(address: string): boolean { - if (address) { - this._breakAddresses.add(address); - return true; - } - return false; - } - - /* - * Clear all data breakpoints. - */ - public clearAllDataBreakpoints(): void { - this._breakAddresses.clear(); - } - - // private methods - - private loadSource(file: string) { - if (this._sourceFile !== file) { - this._sourceFile = file; - - const _textDecoder = new TextDecoder(); - - const uri = vscode.Uri.parse(file); - const content = _textDecoder.decode(this.memfs.readFile(uri)); - this._sourceLines = content.split('\n'); - - //this._sourceLines = readFileSync(this._sourceFile).toString().split('\n'); - } - } - - /** - * Run through the file. - * If stepEvent is specified only run a single step and emit the stepEvent. - */ - private run(reverse = false, stepEvent?: vscode.EventEmitter): void { - if (reverse) { - for (let ln = this._currentLine - 1; ln >= 0; ln--) { - if (this.fireEventsForLine(ln, stepEvent)) { - this._currentLine = ln; - return; - } - } - // no more lines: stop at first line - this._currentLine = 0; - this.stopOnEntry.fire(); - } else { - for (let ln = this._currentLine + 1; ln < this._sourceLines.length; ln++) { - if (this.fireEventsForLine(ln, stepEvent)) { - this._currentLine = ln; - return; - } - } - // no more lines: run to end - this.end.fire(); - } - } - - private verifyBreakpoints(path: string): void { - let bps = this._breakPoints.get(path); - if (bps) { - this.loadSource(path); - bps.forEach(bp => { - if (!bp.verified && bp.line < this._sourceLines.length) { - const srcLine = this._sourceLines[bp.line].trim(); - - // if a line is empty or starts with '+' we don't allow to set a breakpoint but move the breakpoint down - if (srcLine.length === 0 || srcLine.indexOf('+') === 0) { - bp.line++; - } - // if a line starts with '-' we don't allow to set a breakpoint but move the breakpoint up - if (srcLine.indexOf('-') === 0) { - bp.line--; - } - // don't set 'verified' to true if the line contains the word 'lazy' - // in this case the breakpoint will be verified 'lazy' after hitting it once. - if (srcLine.indexOf('lazy') < 0) { - bp.verified = true; - this.breakpointValidated.fire(bp); - } - } - }); - } - } - - /** - * Fire events if line has a breakpoint or the word 'exception' is found. - * Returns true is execution needs to stop. - */ - private fireEventsForLine(ln: number, stepEvent?: vscode.EventEmitter): boolean { - - const line = this._sourceLines[ln].trim(); - - // if 'log(...)' found in source -> send argument to debug console - const matches = /log\((.*)\)/.exec(line); - if (matches && matches.length === 2) { - if (this._sourceFile) { - this.output.fire({ text: matches[1], filePath: this._sourceFile, line: ln, column: matches.index }); - } - } - - // if a word in a line matches a data breakpoint, fire a 'dataBreakpoint' event - const words = line.split(' '); - for (let word of words) { - if (this._breakAddresses.has(word)) { - this.stopOnDataBreakpoint.fire(); - return true; - } - } - - // if word 'exception' found in source -> throw exception - if (line.indexOf('exception') >= 0) { - this.stopOnException.fire(); - return true; - } - - // is there a breakpoint? - const breakpoints = this._sourceFile ? this._breakPoints.get(this._sourceFile) : undefined; - if (breakpoints) { - const bps = breakpoints.filter(bp => bp.line === ln); - if (bps.length > 0) { - - // send 'stopped' event - this.stopOnBreakpoint.fire(); - - // the following shows the use of 'breakpoint' events to update properties of a breakpoint in the UI - // if breakpoint is not yet verified, verify it now and send a 'breakpoint' update event - if (!bps[0].verified) { - bps[0].verified = true; - this.breakpointValidated.fire(bps[0]); - } - return true; - } - } - - // non-empty line - if (stepEvent && line.length > 0) { - stepEvent.fire(); - return true; - } - - // nothing interesting found -> continue - return false; - } -} diff --git a/extensions/vscode-web-playground/src/memfs.ts b/extensions/vscode-web-playground/src/memfs.ts deleted file mode 100644 index 9a135109fb4..00000000000 --- a/extensions/vscode-web-playground/src/memfs.ts +++ /dev/null @@ -1,449 +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, - Disposable, - Event, - EventEmitter, - FileChangeEvent, - FileChangeType, - FileSearchOptions, - FileSearchProvider, - FileSearchQuery, - FileStat, - FileSystemError, - FileSystemProvider, - FileType, - Position, - Progress, - ProviderResult, - Range, - TextSearchComplete, - TextSearchOptions, - TextSearchQuery, - TextSearchProvider, - TextSearchResult, - Uri, - workspace, -} from 'vscode'; -import { largeTSFile, getImageFile, debuggableFile, windows1251File, gbkFile } from './exampleFiles'; - -export class File implements FileStat { - - type: FileType; - ctime: number; - mtime: number; - size: number; - - name: string; - data?: Uint8Array; - - constructor(public uri: Uri, name: string) { - this.type = FileType.File; - this.ctime = Date.now(); - this.mtime = Date.now(); - this.size = 0; - this.name = name; - } -} - -export class Directory implements FileStat { - - type: FileType; - ctime: number; - mtime: number; - size: number; - - name: string; - entries: Map; - - constructor(public uri: Uri, name: string) { - this.type = FileType.Directory; - this.ctime = Date.now(); - this.mtime = Date.now(); - this.size = 0; - this.name = name; - this.entries = new Map(); - } -} - -export type Entry = File | Directory; - -const textEncoder = new TextEncoder(); - -export class MemFS implements FileSystemProvider, FileSearchProvider, TextSearchProvider, Disposable { - static scheme = 'memfs'; - - private readonly disposable: Disposable; - - constructor() { - this.disposable = Disposable.from( - workspace.registerFileSystemProvider(MemFS.scheme, this, { isCaseSensitive: true }), - workspace.registerFileSearchProvider(MemFS.scheme, this), - workspace.registerTextSearchProvider(MemFS.scheme, this) - ); - } - - dispose() { - this.disposable?.dispose(); - } - - seed() { - this.createDirectory(Uri.parse(`memfs:/sample-folder/`)); - - // most common files types - this.writeFile(Uri.parse(`memfs:/sample-folder/large.ts`), textEncoder.encode(largeTSFile), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.txt`), textEncoder.encode('foo'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.html`), textEncoder.encode('

Hello

'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.js`), textEncoder.encode('console.log("JavaScript")'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.json`), textEncoder.encode('{ "json": true }'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.ts`), textEncoder.encode('console.log("TypeScript")'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.css`), textEncoder.encode('* { color: green; }'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.md`), textEncoder.encode(debuggableFile), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.xml`), textEncoder.encode(''), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.py`), textEncoder.encode('import base64, sys; base64.decode(open(sys.argv[1], "rb"), open(sys.argv[2], "wb"))'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.php`), textEncoder.encode('&1\'); ?>'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.yaml`), textEncoder.encode('- just: write something'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.jpg`), getImageFile(), { create: true, overwrite: true }); - - // some more files & folders - this.createDirectory(Uri.parse(`memfs:/sample-folder/folder/`)); - this.createDirectory(Uri.parse(`memfs:/sample-folder/large/`)); - this.createDirectory(Uri.parse(`memfs:/sample-folder/xyz/`)); - this.createDirectory(Uri.parse(`memfs:/sample-folder/xyz/abc`)); - this.createDirectory(Uri.parse(`memfs:/sample-folder/xyz/def`)); - - this.writeFile(Uri.parse(`memfs:/sample-folder/folder/empty.txt`), new Uint8Array(0), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/folder/empty.foo`), new Uint8Array(0), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/folder/file.ts`), textEncoder.encode('let a:number = true; console.log(a);'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/large/rnd.foo`), randomData(50000), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/xyz/UPPER.txt`), textEncoder.encode('UPPER'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/xyz/upper.txt`), textEncoder.encode('upper'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/xyz/def/foo.md`), textEncoder.encode('*MemFS*'), { create: true, overwrite: true }); - - // some files in different encodings - this.createDirectory(Uri.parse(`memfs:/sample-folder/encodings/`)); - this.writeFile( - Uri.parse(`memfs:/sample-folder/encodings/windows1251.txt`), - windows1251File, - { create: true, overwrite: true } - ); - this.writeFile( - Uri.parse(`memfs:/sample-folder/encodings/gbk.txt`), - gbkFile, - { create: true, overwrite: true } - ); - } - - root = new Directory(Uri.parse('memfs:/'), ''); - - // --- manage file metadata - - stat(uri: Uri): FileStat { - return this._lookup(uri, false); - } - - readDirectory(uri: Uri): [string, FileType][] { - const entry = this._lookupAsDirectory(uri, false); - let result: [string, FileType][] = []; - for (const [name, child] of entry.entries) { - result.push([name, child.type]); - } - return result; - } - - // --- manage file contents - - readFile(uri: Uri): Uint8Array { - const data = this._lookupAsFile(uri, false).data; - if (data) { - return data; - } - throw FileSystemError.FileNotFound(); - } - - writeFile(uri: Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): void { - let basename = this._basename(uri.path); - let parent = this._lookupParentDirectory(uri); - let entry = parent.entries.get(basename); - if (entry instanceof Directory) { - throw FileSystemError.FileIsADirectory(uri); - } - if (!entry && !options.create) { - throw FileSystemError.FileNotFound(uri); - } - if (entry && options.create && !options.overwrite) { - throw FileSystemError.FileExists(uri); - } - if (!entry) { - entry = new File(uri, basename); - parent.entries.set(basename, entry); - this._fireSoon({ type: FileChangeType.Created, uri }); - } - entry.mtime = Date.now(); - entry.size = content.byteLength; - entry.data = content; - - this._fireSoon({ type: FileChangeType.Changed, uri }); - } - - // --- manage files/folders - - rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean }): void { - if (!options.overwrite && this._lookup(newUri, true)) { - throw FileSystemError.FileExists(newUri); - } - - let entry = this._lookup(oldUri, false); - let oldParent = this._lookupParentDirectory(oldUri); - - let newParent = this._lookupParentDirectory(newUri); - let newName = this._basename(newUri.path); - - oldParent.entries.delete(entry.name); - entry.name = newName; - newParent.entries.set(newName, entry); - - this._fireSoon( - { type: FileChangeType.Deleted, uri: oldUri }, - { type: FileChangeType.Created, uri: newUri } - ); - } - - delete(uri: Uri): void { - let dirname = uri.with({ path: this._dirname(uri.path) }); - let basename = this._basename(uri.path); - let parent = this._lookupAsDirectory(dirname, false); - if (!parent.entries.has(basename)) { - throw FileSystemError.FileNotFound(uri); - } - parent.entries.delete(basename); - parent.mtime = Date.now(); - parent.size -= 1; - this._fireSoon({ type: FileChangeType.Changed, uri: dirname }, { uri, type: FileChangeType.Deleted }); - } - - createDirectory(uri: Uri): void { - let basename = this._basename(uri.path); - let dirname = uri.with({ path: this._dirname(uri.path) }); - let parent = this._lookupAsDirectory(dirname, false); - - let entry = new Directory(uri, basename); - parent.entries.set(entry.name, entry); - parent.mtime = Date.now(); - parent.size += 1; - this._fireSoon({ type: FileChangeType.Changed, uri: dirname }, { type: FileChangeType.Created, uri }); - } - - // --- lookup - - private _lookup(uri: Uri, silent: false): Entry; - private _lookup(uri: Uri, silent: boolean): Entry | undefined; - private _lookup(uri: Uri, silent: boolean): Entry | undefined { - let parts = uri.path.split('/'); - let entry: Entry = this.root; - for (const part of parts) { - if (!part) { - continue; - } - let child: Entry | undefined; - if (entry instanceof Directory) { - child = entry.entries.get(part); - } - if (!child) { - if (!silent) { - throw FileSystemError.FileNotFound(uri); - } else { - return undefined; - } - } - entry = child; - } - return entry; - } - - private _lookupAsDirectory(uri: Uri, silent: boolean): Directory { - let entry = this._lookup(uri, silent); - if (entry instanceof Directory) { - return entry; - } - throw FileSystemError.FileNotADirectory(uri); - } - - private _lookupAsFile(uri: Uri, silent: boolean): File { - let entry = this._lookup(uri, silent); - if (entry instanceof File) { - return entry; - } - throw FileSystemError.FileIsADirectory(uri); - } - - private _lookupParentDirectory(uri: Uri): Directory { - const dirname = uri.with({ path: this._dirname(uri.path) }); - return this._lookupAsDirectory(dirname, false); - } - - // --- manage file events - - private _emitter = new EventEmitter(); - private _bufferedEvents: FileChangeEvent[] = []; - private _fireSoonHandle?: any; - - readonly onDidChangeFile: Event = this._emitter.event; - - watch(_resource: Uri): Disposable { - // ignore, fires for all changes... - return new Disposable(() => { }); - } - - private _fireSoon(...events: FileChangeEvent[]): void { - this._bufferedEvents.push(...events); - - if (this._fireSoonHandle) { - clearTimeout(this._fireSoonHandle); - } - - this._fireSoonHandle = setTimeout(() => { - this._emitter.fire(this._bufferedEvents); - this._bufferedEvents.length = 0; - }, 5); - } - - // --- path utils - - private _basename(path: string): string { - path = this._rtrim(path, '/'); - if (!path) { - return ''; - } - - return path.substr(path.lastIndexOf('/') + 1); - } - - private _dirname(path: string): string { - path = this._rtrim(path, '/'); - if (!path) { - return '/'; - } - - return path.substr(0, path.lastIndexOf('/')); - } - - private _rtrim(haystack: string, needle: string): string { - if (!haystack || !needle) { - return haystack; - } - - const needleLen = needle.length, - haystackLen = haystack.length; - - if (needleLen === 0 || haystackLen === 0) { - return haystack; - } - - let offset = haystackLen, - idx = -1; - - while (true) { - idx = haystack.lastIndexOf(needle, offset - 1); - if (idx === -1 || idx + needleLen !== offset) { - break; - } - if (idx === 0) { - return ''; - } - offset = idx; - } - - return haystack.substring(0, offset); - } - - private _getFiles(): Set { - const files = new Set(); - - this._doGetFiles(this.root, files); - - return files; - } - - private _doGetFiles(dir: Directory, files: Set): void { - dir.entries.forEach(entry => { - if (entry instanceof File) { - files.add(entry); - } else { - this._doGetFiles(entry, files); - } - }); - } - - private _convertSimple2RegExpPattern(pattern: string): string { - return pattern.replace(/[\-\\\{\}\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, '\\$&').replace(/[\*]/g, '.*'); - } - - // --- search provider - - provideFileSearchResults(query: FileSearchQuery, _options: FileSearchOptions, _token: CancellationToken): ProviderResult { - return this._findFiles(query.pattern); - } - - private _findFiles(query: string | undefined): Uri[] { - const files = this._getFiles(); - const result: Uri[] = []; - - const pattern = query ? new RegExp(this._convertSimple2RegExpPattern(query)) : null; - - for (const file of files) { - if (!pattern || pattern.exec(file.name)) { - result.push(file.uri); - } - } - - return result; - } - - private _textDecoder = new TextDecoder(); - - provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, _token: CancellationToken) { - const result: TextSearchComplete = { limitHit: false }; - - const files = this._findFiles(options.includes[0]); - if (files) { - for (const file of files) { - const content = this._textDecoder.decode(this.readFile(file)); - - const lines = content.split('\n'); - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const index = line.indexOf(query.pattern); - if (index !== -1) { - progress.report({ - uri: file, - ranges: new Range(new Position(i, index), new Position(i, index + query.pattern.length)), - preview: { - text: line, - matches: new Range(new Position(0, index), new Position(0, index + query.pattern.length)) - } - }); - } - } - } - } - - return result; - } -} - -function randomData(lineCnt: number, lineLen = 155): Uint8Array { - let lines: string[] = []; - for (let i = 0; i < lineCnt; i++) { - let line = ''; - while (line.length < lineLen) { - line += Math.random().toString(2 + (i % 34)).substr(2); - } - lines.push(line.substr(0, lineLen)); - } - return textEncoder.encode(lines.join('\n')); -} diff --git a/extensions/vscode-web-playground/src/typings/ref.d.ts b/extensions/vscode-web-playground/src/typings/ref.d.ts deleted file mode 100644 index 9abc416f7e8..00000000000 --- a/extensions/vscode-web-playground/src/typings/ref.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/// -/// -/// -/// -/// diff --git a/extensions/vscode-web-playground/tsconfig.json b/extensions/vscode-web-playground/tsconfig.json deleted file mode 100644 index 633da7fad77..00000000000 --- a/extensions/vscode-web-playground/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../shared.tsconfig.json", - "compilerOptions": { - "outDir": "./out", - "lib": [ - "dom", - "dom.iterable", - "es2018" - ] - }, - "include": [ - "src/**/*" - ] -} diff --git a/extensions/vscode-web-playground/yarn.lock b/extensions/vscode-web-playground/yarn.lock deleted file mode 100644 index b29fc8fc61d..00000000000 --- a/extensions/vscode-web-playground/yarn.lock +++ /dev/null @@ -1,109 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@types/mocha@2.2.43": - version "2.2.43" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.43.tgz#03c54589c43ad048cbcbfd63999b55d0424eec27" - integrity sha512-xNlAmH+lRJdUMXClMTI9Y0pRqIojdxfm7DHsIxoB2iTzu3fnPmSMEN8SsSx0cdwV36d02PWCWaDUoZPDSln+xw== - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -charenc@~0.0.1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" - integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= - -crypt@~0.0.1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" - integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= - -debug@^2.2.0: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^3.1.0: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - -is-buffer@~1.1.1: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -lodash@^4.16.4: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== - -md5@^2.1.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" - integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk= - dependencies: - charenc "~0.0.1" - crypt "~0.0.1" - is-buffer "~1.1.1" - -minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -mkdirp@~0.5.1: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - -mocha-junit-reporter@^1.17.0: - version "1.23.3" - resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.23.3.tgz#941e219dd759ed732f8641e165918aa8b167c981" - integrity sha512-ed8LqbRj1RxZfjt/oC9t12sfrWsjZ3gNnbhV1nuj9R/Jb5/P3Xb4duv2eCfCDMYH+fEu0mqca7m4wsiVjsxsvA== - dependencies: - debug "^2.2.0" - md5 "^2.1.0" - mkdirp "~0.5.1" - strip-ansi "^4.0.0" - xml "^1.0.0" - -mocha-multi-reporters@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/mocha-multi-reporters/-/mocha-multi-reporters-1.1.7.tgz#cc7f3f4d32f478520941d852abb64d9988587d82" - integrity sha1-zH8/TTL0eFIJQdhSq7ZNmYhYfYI= - dependencies: - debug "^3.1.0" - lodash "^4.16.4" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -xml@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" - integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU= diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 102d128edb6..8ed194dd356 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -typescript@3.9.7: - version "3.9.7" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" - integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== +typescript@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2" + integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== diff --git a/package.json b/package.json index 80325e14c5d..a90ba51a693 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.49.0", - "distro": "db6c4e7925a5077a6e61690a057fa6cf139e0e49", + "distro": "bcdce5838f1e576a4d23e2ad8907828f1dbd2121", "author": { "name": "Microsoft Corporation" }, @@ -40,7 +40,7 @@ "valid-layers-check": "node build/lib/layersChecker.js", "strict-function-types-watch": "tsc --watch -p src/tsconfig.json --noEmit --strictFunctionTypes", "update-distro": "node build/npm/update-distro.js", - "web": "node resources/serverless/code-web.js", + "web": "node resources/web/code-web.js", "compile-web": "gulp compile-web --max_old_space_size=4095", "watch-web": "gulp watch-web --max_old_space_size=4095", "eslint": "eslint -c .eslintrc.json --rulesdir ./build/lib/eslint --ext .ts --ext .js ./src/vs ./extensions" @@ -56,7 +56,7 @@ "keytar": "^5.5.0", "minimist": "^1.2.5", "native-is-elevated": "0.4.1", - "native-keymap": "2.1.2", + "native-keymap": "2.2.0", "native-watchdog": "1.3.0", "node-pty": "0.10.0-beta8", "semver-umd": "^5.5.7", @@ -109,7 +109,7 @@ "css-loader": "^3.2.0", "debounce": "^1.0.0", "deemon": "^1.4.0", - "electron": "7.3.2", + "electron": "9.2.1", "eslint": "6.8.0", "eslint-plugin-jsdoc": "^19.1.0", "event-stream": "3.3.4", @@ -164,7 +164,7 @@ "source-map": "^0.4.4", "style-loader": "^1.0.0", "ts-loader": "^4.4.2", - "typescript": "^4.0.0-dev.20200803", + "typescript": "^4.1.0-dev.20200824", "typescript-formatter": "7.1.0", "underscore": "^1.8.2", "vinyl": "^2.0.0", diff --git a/product.json b/product.json index 2b884d18f30..640b9100707 100644 --- a/product.json +++ b/product.json @@ -31,7 +31,7 @@ "builtInExtensions": [ { "name": "ms-vscode.node-debug", - "version": "1.44.8", + "version": "1.44.11", "repo": "https://github.com/Microsoft/vscode-node-debug", "metadata": { "id": "b6ded8fb-a0a0-4c1c-acbd-ab2a3bc995a6", @@ -61,7 +61,7 @@ }, { "name": "ms-vscode.references-view", - "version": "0.0.61", + "version": "0.0.62", "repo": "https://github.com/Microsoft/vscode-reference-view", "metadata": { "id": "dc489f46-520d-4556-ae85-1f9eab3c412d", @@ -76,7 +76,7 @@ }, { "name": "ms-vscode.js-debug-companion", - "version": "1.0.2", + "version": "1.0.6", "repo": "https://github.com/microsoft/vscode-js-debug-companion", "metadata": { "id": "99cb0b7f-7354-4278-b8da-6cc79972169d", @@ -91,7 +91,7 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.48.1", + "version": "1.49.2", "repo": "https://github.com/Microsoft/vscode-js-debug", "metadata": { "id": "25629058-ddac-4e17-abba-74678e126c5d", @@ -123,7 +123,7 @@ "webBuiltInExtensions": [ { "name": "ms-vscode.github-browser", - "version": "0.0.2", + "version": "0.0.5", "repo": "https://github.com/Microsoft/vscode-github-browser", "metadata": { "id": "c1bcff4b-4ecb-466e-b8f6-b02788b5fb5a", diff --git a/remote/.yarnrc b/remote/.yarnrc index 1e16cde724c..c1a32ce532a 100644 --- a/remote/.yarnrc +++ b/remote/.yarnrc @@ -1,3 +1,3 @@ disturl "http://nodejs.org/dist" -target "12.4.0" +target "12.14.1" runtime "node" diff --git a/resources/serverless/callback.html b/resources/web/callback.html similarity index 100% rename from resources/serverless/callback.html rename to resources/web/callback.html diff --git a/resources/serverless/code-web.js b/resources/web/code-web.js similarity index 83% rename from resources/serverless/code-web.js rename to resources/web/code-web.js index 90d81b3fb9d..58f3e586539 100644 --- a/resources/serverless/code-web.js +++ b/resources/web/code-web.js @@ -16,14 +16,19 @@ const opn = require('opn'); const minimist = require('minimist'); const fancyLog = require('fancy-log'); const ansiColors = require('ansi-colors'); +const remote = require('gulp-remote-retry-src'); +const vfs = require('vinyl-fs'); const extensions = require('../../build/lib/extensions'); const APP_ROOT = path.join(__dirname, '..', '..'); const BUILTIN_EXTENSIONS_ROOT = path.join(APP_ROOT, 'extensions'); const BUILTIN_MARKETPLACE_EXTENSIONS_ROOT = path.join(APP_ROOT, '.build', 'builtInExtensions'); +const WEB_DEV_EXTENSIONS_ROOT = path.join(APP_ROOT, '.build', 'builtInWebDevExtensions'); const WEB_MAIN = path.join(APP_ROOT, 'src', 'vs', 'code', 'browser', 'workbench', 'workbench-dev.html'); +const WEB_PLAYGROUND_VERSION = '0.0.5'; + const args = minimist(process.argv, { boolean: [ 'no-launch', @@ -72,9 +77,10 @@ async function getBuiltInExtensionInfos() { /** @type {Object.} */ const locations = {}; - const [localExtensions, marketplaceExtensions] = await Promise.all([ + const [localExtensions, marketplaceExtensions, webDevExtensions] = await Promise.all([ extensions.scanBuiltinExtensions(BUILTIN_EXTENSIONS_ROOT), extensions.scanBuiltinExtensions(BUILTIN_MARKETPLACE_EXTENSIONS_ROOT), + ensureWebDevExtensions().then(() => extensions.scanBuiltinExtensions(WEB_DEV_EXTENSIONS_ROOT)) ]); for (const ext of localExtensions) { allExtensions.push(ext); @@ -84,6 +90,10 @@ async function getBuiltInExtensionInfos() { allExtensions.push(ext); locations[ext.extensionPath] = path.join(BUILTIN_MARKETPLACE_EXTENSIONS_ROOT, ext.extensionPath); } + for (const ext of webDevExtensions) { + allExtensions.push(ext); + locations[ext.extensionPath] = path.join(WEB_DEV_EXTENSIONS_ROOT, ext.extensionPath); + } for (const ext of allExtensions) { if (ext.packageJSON.browser) { let mainFilePath = path.join(locations[ext.extensionPath], ext.packageJSON.browser); @@ -98,7 +108,43 @@ async function getBuiltInExtensionInfos() { return { extensions: allExtensions, locations }; } -async function getDefaultExtensionInfos() { +async function ensureWebDevExtensions() { + + // Playground (https://github.com/microsoft/vscode-web-playground) + const webDevPlaygroundRoot = path.join(WEB_DEV_EXTENSIONS_ROOT, 'vscode-web-playground'); + const webDevPlaygroundExists = await exists(webDevPlaygroundRoot); + + let downloadPlayground = false; + if (webDevPlaygroundExists) { + try { + const webDevPlaygroundPackageJson = JSON.parse(((await readFile(path.join(webDevPlaygroundRoot, 'package.json'))).toString())); + if (webDevPlaygroundPackageJson.version !== WEB_PLAYGROUND_VERSION) { + downloadPlayground = true; + } + } catch (error) { + downloadPlayground = true; + } + } else { + downloadPlayground = true; + } + + if (downloadPlayground) { + if (args.verbose) { + fancyLog(`${ansiColors.magenta('Web Development extensions')}: Downloading vscode-web-playground to ${webDevPlaygroundRoot}`); + } + await new Promise((resolve, reject) => { + remote(['package.json', 'dist/extension.js', 'dist/extension.js.map'], { + base: 'https://raw.githubusercontent.com/microsoft/vscode-web-playground/main/' + }).pipe(vfs.dest(webDevPlaygroundRoot)).on('end', resolve).on('error', reject); + }); + } else { + if (args.verbose) { + fancyLog(`${ansiColors.magenta('Web Development extensions')}: Using existing vscode-web-playground in ${webDevPlaygroundRoot}`); + } + } +} + +async function getCommandlineProvidedExtensionInfos() { const extensions = []; /** @type {Object.} */ @@ -150,7 +196,7 @@ async function getExtensionPackageJSON(extensionPath) { } const builtInExtensionsPromise = getBuiltInExtensionInfos(); -const defaultExtensionsPromise = getDefaultExtensionInfos(); +const commandlineProvidedExtensionsPromise = getCommandlineProvidedExtensionInfos(); const mapCallbackUriToRequestId = new Map(); @@ -245,7 +291,7 @@ async function handleStatic(req, res, parsedUrl) { async function handleExtension(req, res, parsedUrl) { // Strip `/extension/` from the path const relativePath = decodeURIComponent(parsedUrl.pathname.substr('/extension/'.length)); - const filePath = getExtensionFilePath(relativePath, (await defaultExtensionsPromise).locations); + const filePath = getExtensionFilePath(relativePath, (await commandlineProvidedExtensionsPromise).locations); if (!filePath) { return serveError(req, res, 400, `Bad request.`); } @@ -289,10 +335,21 @@ async function handleRoot(req, res) { } const { extensions: builtInExtensions } = await builtInExtensionsPromise; - const { extensions: staticExtensions } = await defaultExtensionsPromise; + const { extensions: staticExtensions, locations: staticLocations } = await commandlineProvidedExtensionsPromise; + + const dedupedBuiltInExtensions = []; + for (const builtInExtension of builtInExtensions) { + const extensionId = `${builtInExtension.packageJSON.publisher}.${builtInExtension.packageJSON.name}`; + if (staticLocations[extensionId]) { + fancyLog(`${ansiColors.magenta('BuiltIn extensions')}: Ignoring built-in ${extensionId} because it was overridden via --extension argument`); + continue; + } + + dedupedBuiltInExtensions.push(builtInExtension); + } if (args.verbose) { - fancyLog(`${ansiColors.magenta('BuiltIn extensions')}: ${builtInExtensions.map(e => path.basename(e.extensionPath)).join(', ')}`); + fancyLog(`${ansiColors.magenta('BuiltIn extensions')}: ${dedupedBuiltInExtensions.map(e => path.basename(e.extensionPath)).join(', ')}`); fancyLog(`${ansiColors.magenta('Additional extensions')}: ${staticExtensions.map(e => path.basename(e.extensionLocation.path)).join(', ') || 'None'}`); } @@ -306,7 +363,7 @@ async function handleRoot(req, res) { const data = (await readFile(WEB_MAIN)).toString() .replace('{{WORKBENCH_WEB_CONFIGURATION}}', () => escapeAttribute(JSON.stringify(webConfigJSON))) // use a replace function to avoid that regexp replace patterns ($&, $0, ...) are applied - .replace('{{WORKBENCH_BUILTIN_EXTENSIONS}}', () => escapeAttribute(JSON.stringify(builtInExtensions))) + .replace('{{WORKBENCH_BUILTIN_EXTENSIONS}}', () => escapeAttribute(JSON.stringify(dedupedBuiltInExtensions))) .replace('{{WEBVIEW_ENDPOINT}}', '') .replace('{{REMOTE_USER_DATA_URI}}', ''); @@ -351,7 +408,7 @@ async function handleCallback(req, res, parsedUrl) { // add to map of known callbacks mapCallbackUriToRequestId.set(requestId, JSON.stringify({ scheme: vscodeScheme || 'code-oss', authority: vscodeAuthority, path: vscodePath, query, fragment: vscodeFragment })); - return serveFile(req, res, path.join(APP_ROOT, 'resources', 'serverless', 'callback.html'), { 'Content-Type': 'text/html' }); + return serveFile(req, res, path.join(APP_ROOT, 'resources', 'web', 'callback.html'), { 'Content-Type': 'text/html' }); } /** diff --git a/resources/win32/bin/code.sh b/resources/win32/bin/code.sh index 9f029e5522a..d86b6e0574a 100644 --- a/resources/win32/bin/code.sh +++ b/resources/win32/bin/code.sh @@ -37,7 +37,7 @@ else fi if [ $IN_WSL = true ]; then - export WSLENV=ELECTRON_RUN_AS_NODE/w:$WSLENV + export WSLENV="ELECTRON_RUN_AS_NODE/w:$WSLENV" CLI=$(wslpath -m "$VSCODE_PATH/resources/app/out/cli.js") # use the Remote WSL extension if installed diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 3133c7869a5..5a493b0b1c5 100644 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -72,8 +72,11 @@ mkdir %GITWORKSPACE% call "%INTEGRATION_TEST_ELECTRON_PATH%" %GITWORKSPACE% --extensionDevelopmentPath=%~dp0\..\extensions\git --extensionTestsPath=%~dp0\..\extensions\git\out\test --enable-proposed-api=vscode.git --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% if %errorlevel% neq 0 exit /b %errorlevel% -:: Tests in commonJS (HTML, CSS, JSON language server tests...) -call .\scripts\node-electron.bat .\node_modules\mocha\bin\_mocha .\extensions\*\server\out\test\**\*.test.js +:: Tests in commonJS (CSS, HTML) +call %~dp0\node-electron.bat %~dp0\..\extensions\css-language-features/server/test/index.js +if %errorlevel% neq 0 exit /b %errorlevel% + +call %~dp0\node-electron.bat %~dp0\..\extensions\html-language-features/server/test/index.js if %errorlevel% neq 0 exit /b %errorlevel% rmdir /s /q %VSCODEUSERDATADIR% diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index 5412a5c0ecd..6856ebd525f 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -59,7 +59,7 @@ fi "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $(mktemp -d 2>/dev/null) --enable-proposed-api=vscode.git --extensionDevelopmentPath=$ROOT/extensions/git --extensionTestsPath=$ROOT/extensions/git/out/test --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-notebook-tests/test --enable-proposed-api=vscode.vscode-notebook-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-notebook-tests --extensionTestsPath=$ROOT/extensions/vscode-notebook-tests/out/ --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR -# Tests in commonJS +# Tests in commonJS (CSS, HTML) cd $ROOT/extensions/css-language-features/server && $ROOT/scripts/node-electron.sh test/index.js cd $ROOT/extensions/html-language-features/server && $ROOT/scripts/node-electron.sh test/index.js diff --git a/src/buildfile.js b/src/buildfile.js index 2df9bf24e73..f6d1c647d9d 100644 --- a/src/buildfile.js +++ b/src/buildfile.js @@ -16,6 +16,7 @@ exports.base = [{ }]; exports.workerExtensionHost = [entrypoint('vs/workbench/services/extensions/worker/extensionHostWorker')]; +exports.workerNotebook = [entrypoint('vs/workbench/contrib/notebook/common/services/notebookSimpleWorker')]; exports.workbenchDesktop = require('./vs/workbench/buildfile.desktop').collectModules(); exports.workbenchWeb = require('./vs/workbench/buildfile.web').collectModules(); diff --git a/src/main.js b/src/main.js index f179a672031..e4662daa6b4 100644 --- a/src/main.js +++ b/src/main.js @@ -18,7 +18,11 @@ const bootstrap = require('./bootstrap'); const paths = require('./paths'); /** @type {any} */ const product = require('../product.json'); -const { app, protocol } = require('electron'); +const { app, protocol, crashReporter } = require('electron'); + +// Disable render process reuse, we still have +// non-context aware native modules in the renderer. +app.allowRendererProcessReuse = false; // Enable portable support const portable = bootstrap.configurePortable(product); @@ -31,13 +35,13 @@ const args = parseCLIArgs(); const userDataPath = getUserDataPath(args); app.setPath('userData', userDataPath); -// Set temp directory based on crash-reporter-directory CLI argument -// The crash reporter will store crashes in temp folder so we need -// to change that location accordingly. +// Configure static command line arguments +const argvConfig = configureCommandlineSwitchesSync(args); -// If a crash-reporter-directory is specified we setup the crash reporter -// right from the beginning as early as possible to monitor all processes. +// If a crash-reporter-directory is specified we store the crash reports +// in the specified directory and don't upload them to the crash server. let crashReporterDirectory = args['crash-reporter-directory']; +let submitURL = ''; if (crashReporterDirectory) { crashReporterDirectory = path.normalize(crashReporterDirectory); @@ -55,23 +59,41 @@ if (crashReporterDirectory) { } } - // Crashes are stored in the temp directory by default, so we + // Crashes are stored in the crashDumps directory by default, so we // need to change that directory to the provided one - console.log(`Found --crash-reporter-directory argument. Setting temp directory to be '${crashReporterDirectory}'`); - app.setPath('temp', crashReporterDirectory); - - // Start crash reporter - const { crashReporter } = require('electron'); - const productName = (product.crashReporter && product.crashReporter.productName) || product.nameShort; - const companyName = (product.crashReporter && product.crashReporter.companyName) || 'Microsoft'; - crashReporter.start({ - companyName: companyName, - productName: process.env['VSCODE_DEV'] ? `${productName} Dev` : productName, - submitURL: '', - uploadToServer: false - }); + console.log(`Found --crash-reporter-directory argument. Setting crashDumps directory to be '${crashReporterDirectory}'`); + app.setPath('crashDumps', crashReporterDirectory); +} else { + const appCenter = product.appCenter; + // Disable Appcenter crash reporting if + // * --crash-reporter-directory is specified + // * enable-crash-reporter runtime argument is set to 'false' + // * --disable-crash-reporter command line parameter is set + if (appCenter && argvConfig['enable-crash-reporter'] && !args['disable-crash-reporter']) { + const isWindows = (process.platform === 'win32'); + const isLinux = (process.platform === 'linux'); + const crashReporterId = argvConfig['crash-reporter-id']; + const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + if (uuidPattern.test(crashReporterId)) { + submitURL = isWindows ? appCenter[process.arch === 'ia32' ? 'win32-ia32' : 'win32-x64'] : isLinux ? appCenter[`linux-x64`] : appCenter.darwin; + submitURL = submitURL.concat('&uid=', crashReporterId, '&iid=', crashReporterId, '&sid=', crashReporterId); + // Send the id for child node process that are explicitly starting crash reporter. + // For vscode this is ExtensionHost process currently. + process.argv.push('--crash-reporter-id', crashReporterId); + } + } } +// Start crash reporter for all processes +const productName = (product.crashReporter ? product.crashReporter.productName : undefined) || product.nameShort; +const companyName = (product.crashReporter ? product.crashReporter.companyName : undefined) || 'Microsoft'; +crashReporter.start({ + companyName: companyName, + productName: process.env['VSCODE_DEV'] ? `${productName} Dev` : productName, + submitURL, + uploadToServer: !crashReporterDirectory +}); + // Set logs path before app 'ready' event if running portable // to ensure that no 'logs' folder is created on disk at a // location outside of the portable directory @@ -110,9 +132,6 @@ registerListeners(); // Cached data const nodeCachedDataDir = getNodeCachedDir(); -// Configure static command line arguments -const argvConfig = configureCommandlineSwitchesSync(args); - // Remove env set by snap https://github.com/microsoft/vscode/issues/85344 if (process.env['SNAP']) { delete process.env['GDK_PIXBUF_MODULE_FILE']; @@ -254,9 +273,6 @@ function configureCommandlineSwitchesSync(cliArgs) { app.commandLine.appendSwitch('js-flags', jsFlags); } - // TODO@Deepak Electron 7 workaround for https://github.com/microsoft/vscode/issues/88873 - app.commandLine.appendSwitch('disable-features', 'LayoutNG'); - return argvConfig; } @@ -297,21 +313,10 @@ function createDefaultArgvConfigSync(argvConfigPath) { fs.mkdirSync(argvConfigPathDirname); } - // Migrate over legacy locale - const localeConfigPath = path.join(userDataPath, 'User', 'locale.json'); - const legacyLocale = getLegacyUserDefinedLocaleSync(localeConfigPath); - if (legacyLocale) { - try { - fs.unlinkSync(localeConfigPath); - } catch (error) { - //ignore - } - } - // Default argv content const defaultArgvConfigContent = [ '// This configuration file allows you to pass permanent command line arguments to VS Code.', - '// Only a subset of arguments is currently supported to reduce the likelyhood of breaking', + '// Only a subset of arguments is currently supported to reduce the likelihood of breaking', '// the installation.', '//', '// PLEASE DO NOT CHANGE WITHOUT UNDERSTANDING THE IMPACT', @@ -324,19 +329,10 @@ function createDefaultArgvConfigSync(argvConfigPath) { '', ' // Enabled by default by VS Code to resolve color issues in the renderer', ' // See https://github.com/Microsoft/vscode/issues/51791 for details', - ' "disable-color-correct-rendering": true' + ' "disable-color-correct-rendering": true', + '}' ]; - if (legacyLocale) { - defaultArgvConfigContent[defaultArgvConfigContent.length - 1] = `${defaultArgvConfigContent[defaultArgvConfigContent.length - 1]},`; // append trailing "," - - defaultArgvConfigContent.push(''); - defaultArgvConfigContent.push(' // Display language of VS Code'); - defaultArgvConfigContent.push(` "locale": "${legacyLocale}"`); - } - - defaultArgvConfigContent.push('}'); - // Create initial argv.json with default content fs.writeFileSync(argvConfigPath, defaultArgvConfigContent.join('\n')); } catch (error) { @@ -594,19 +590,4 @@ function getUserDefinedLocale(argvConfig) { return argvConfig.locale && typeof argvConfig.locale === 'string' ? argvConfig.locale.toLowerCase() : undefined; } -/** - * @param {string} localeConfigPath - * @returns {string | undefined} - */ -function getLegacyUserDefinedLocaleSync(localeConfigPath) { - try { - const content = stripComments(fs.readFileSync(localeConfigPath).toString()); - - const value = JSON.parse(content).locale; - return value && typeof value === 'string' ? value.toLowerCase() : undefined; - } catch (error) { - // ignore - } -} - //#endregion diff --git a/src/tsconfig.base.json b/src/tsconfig.base.json index 19165d97b72..6decaae056e 100644 --- a/src/tsconfig.base.json +++ b/src/tsconfig.base.json @@ -5,6 +5,7 @@ "experimentalDecorators": true, "noImplicitReturns": true, "noUnusedLocals": true, + "allowUnreachableCode": false, "strict": true, "forceConsistentCasingInFileNames": true, "baseUrl": ".", @@ -15,6 +16,7 @@ }, "lib": [ "ES2015", + "ES2016.Array.Include", "ES2017.String", "ES2018.Promise", "DOM", diff --git a/src/tsconfig.monaco.json b/src/tsconfig.monaco.json index 825a83761f2..86b2926a1ab 100644 --- a/src/tsconfig.monaco.json +++ b/src/tsconfig.monaco.json @@ -15,7 +15,6 @@ "include": [ "typings/require.d.ts", "typings/thenable.d.ts", - "typings/lib.array-ext.d.ts", "vs/css.d.ts", "vs/monaco.d.ts", "vs/nls.d.ts", diff --git a/src/typings/lib.array-ext.d.ts b/src/typings/lib.array-ext.d.ts deleted file mode 100644 index 5a77b70a9f2..00000000000 --- a/src/typings/lib.array-ext.d.ts +++ /dev/null @@ -1,11 +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 ArrayConstructor { - isArray(arg: ReadonlyArray | null | undefined): arg is ReadonlyArray; - isArray(arg: Array | null | undefined): arg is Array; - isArray(arg: any): arg is Array; - isArray(arg: any): arg is Array; -} \ No newline at end of file diff --git a/src/typings/node.processEnv-ext.d.ts b/src/typings/node.processEnv-ext.d.ts deleted file mode 100644 index fec557ff2a7..00000000000 --- a/src/typings/node.processEnv-ext.d.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. - *--------------------------------------------------------------------------------------------*/ - -declare namespace NodeJS { - - export interface Process { - - /** - * The lazy enviroment is a promise that resolves to `process.env` - * once the process is resolved. The use-case is VS Code running - * on Linux/macOS when being launched via a launcher. Then the env - * (as defined in .bashrc etc) isn't properly set and needs to be - * resolved lazy. - */ - lazyEnv: Thenable | undefined; - } -} diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index f203119f8f6..9349efc2792 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -23,6 +23,9 @@ export function clearNode(node: HTMLElement): void { } } +/** + * @deprecated use `node.remove()` instead + */ export function removeNode(node: HTMLElement): void { if (node.parentNode) { node.parentNode.removeChild(node); @@ -994,6 +997,11 @@ export function trackFocus(element: HTMLElement | Window): IFocusTracker { return new FocusTracker(element); } +export function after(sibling: HTMLElement, child: T): T { + sibling.after(child); + return child; +} + export function append(parent: HTMLElement, ...children: T[]): T { children.forEach(child => parent.appendChild(child)); return children[children.length - 1]; @@ -1004,7 +1012,19 @@ export function prepend(parent: HTMLElement, child: T): T { return child; } -const SELECTOR_REGEX = /([\w\-]+)?(#([\w\-]+))?((.([\w\-]+))*)/; +const SELECTOR_REGEX = /([\w\-]+)?(#([\w\-]+))?((\.([\w\-]+))*)/; + +export function reset(parent: HTMLElement, ...children: Array) { + parent.innerText = ''; + coalesce(children) + .forEach(child => { + if (child instanceof Node) { + parent.appendChild(child); + } else { + parent.appendChild(document.createTextNode(child as string)); + } + }); +} export enum Namespace { HTML = 'http://www.w3.org/1999/xhtml', diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index e79f159100b..48ead97d48f 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -17,6 +17,7 @@ import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { renderCodicons, markdownEscapeEscapedCodicons } from 'vs/base/common/codicons'; import { resolvePath } from 'vs/base/common/resources'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; export interface MarkedOptions extends marked.MarkedOptions { baseUrl?: never; @@ -171,25 +172,32 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende const actionHandler = options.actionHandler; if (actionHandler) { - actionHandler.disposeables.add(DOM.addStandardDisposableListener(element, 'click', event => { - let target: HTMLElement | null = event.target; - if (target.tagName !== 'A') { - target = target.parentElement; - if (!target || target.tagName !== 'A') { + [DOM.EventType.CLICK, DOM.EventType.AUXCLICK].forEach(event => { + actionHandler.disposeables.add(DOM.addDisposableListener(element, event, (e: MouseEvent) => { + const mouseEvent = new StandardMouseEvent(e); + if (!mouseEvent.leftButton && !mouseEvent.middleButton) { return; } - } - try { - const href = target.dataset['href']; - if (href) { - actionHandler.callback(href, event); + + let target: HTMLElement | null = mouseEvent.target; + if (target.tagName !== 'A') { + target = target.parentElement; + if (!target || target.tagName !== 'A') { + return; + } } - } catch (err) { - onUnexpectedError(err); - } finally { - event.preventDefault(); - } - })); + try { + const href = target.dataset['href']; + if (href) { + actionHandler.callback(href, mouseEvent); + } + } catch (err) { + onUnexpectedError(err); + } finally { + mouseEvent.preventDefault(); + } + })); + }); } // Use our own sanitizer so that we can let through only spans. diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index bbc0918db24..aef1d1c1e1d 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -285,6 +285,10 @@ export class ActionBar extends Disposable implements IActionRunner { index++; } }); + if (this.focusedItem) { + // After a clear actions might be re-added to simply toggle some actions. We should preserve focus #97128 + this.focus(this.focusedItem); + } } getWidth(index: number): number { diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index 93c4230477b..de211c787a3 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -187,14 +187,14 @@ class Label { if (typeof label === 'string') { if (!this.singleLabel) { - this.container.innerHTML = ''; + this.container.innerText = ''; dom.removeClass(this.container, 'multiple'); this.singleLabel = dom.append(this.container, dom.$('a.label-name', { id: options?.domId })); } this.singleLabel.textContent = label; } else { - this.container.innerHTML = ''; + this.container.innerText = ''; dom.addClass(this.container, 'multiple'); this.singleLabel = undefined; @@ -250,7 +250,7 @@ class LabelWithHighlights { if (typeof label === 'string') { if (!this.singleLabel) { - this.container.innerHTML = ''; + this.container.innerText = ''; dom.removeClass(this.container, 'multiple'); this.singleLabel = new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId })), this.supportCodicons); } @@ -258,7 +258,7 @@ class LabelWithHighlights { this.singleLabel.set(label, options?.matches, options?.title, options?.labelEscapeNewLines); } else { - this.container.innerHTML = ''; + this.container.innerText = ''; dom.addClass(this.container, 'multiple'); this.singleLabel = undefined; diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index a910e375661..2410c5234bb 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -170,7 +170,7 @@ export class InputBox extends Widget { this.maxHeight = typeof this.options.flexibleMaxHeight === 'number' ? this.options.flexibleMaxHeight : Number.POSITIVE_INFINITY; this.mirror = dom.append(wrapper, $('div.mirror')); - this.mirror.innerHTML = ' '; + this.mirror.innerText = '\u00a0'; this.scrollableElement = new ScrollableElement(this.element, { vertical: ScrollbarVisibility.Auto }); @@ -529,7 +529,7 @@ export class InputBox extends Widget { if (mirrorTextContent) { this.mirror.textContent = value + suffix; } else { - this.mirror.innerHTML = ' '; + this.mirror.innerText = '\u00a0'; } this.layout(); diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 5cecaeb44e4..9821d6bcc6f 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -121,7 +121,7 @@ export class ExternalElementsDragAndDropData implements IDragAndDropData { } } -export class DesktopDragAndDropData implements IDragAndDropData { +export class NativeDragAndDropData implements IDragAndDropData { readonly types: any[]; readonly files: any[]; @@ -602,6 +602,10 @@ export class ListView implements ISpliceable, IDisposable { return this.items[index].element; } + indexOf(element: T): number { + return this.items.findIndex(item => item.element === element); + } + domElement(index: number): HTMLElement | null { const row = this.items[index].row; return row && row.domNode; @@ -972,7 +976,7 @@ export class ListView implements ISpliceable, IDisposable { return false; } - this.currentDragData = new DesktopDragAndDropData(); + this.currentDragData = new NativeDragAndDropData(); } } diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index d7e466466bd..252150b28d7 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -1323,6 +1323,10 @@ export class List implements ISpliceable, IThemable, IDisposable { return this.view.element(index); } + indexOf(element: T): number { + return this.view.indexOf(element); + } + get length(): number { return this.view.length; } diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index 028c8c157be..f7e9bea946b 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -324,8 +324,7 @@ export class Menu extends ActionBar { if (action instanceof Separator) { return new MenuSeparatorActionViewItem(options.context, action, { icon: true }); } else if (action instanceof SubmenuAction) { - const actions = Array.isArray(action.actions) ? action.actions : action.actions(); - const menuActionViewItem = new SubmenuMenuActionViewItem(action, actions, parentData, { ...options, submenuIds: new Set([...(options.submenuIds || []), action.id]) }); + const menuActionViewItem = new SubmenuMenuActionViewItem(action, action.actions, parentData, { ...options, submenuIds: new Set([...(options.submenuIds || []), action.id]) }); if (options.enableMnemonics) { const mnemonic = menuActionViewItem.getMnemonic(); @@ -791,7 +790,12 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem { private cleanupExistingSubmenu(force: boolean): void { if (this.parentData.submenu && (force || (this.parentData.submenu !== this.mysubmenu))) { - this.parentData.submenu.dispose(); + + // disposal may throw if the submenu has already been removed + try { + this.parentData.submenu.dispose(); + } catch { } + this.parentData.submenu = undefined; this.updateAriaExpanded('false'); if (this.submenuContainer) { diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index c54ff189085..a8f9bb24c5a 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -215,8 +215,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi // Intercept keyboard handling - // React on KEY_UP since the actionBar also reacts on KEY_UP so that appropriate events get canceled - this._register(dom.addDisposableListener(this.selectElement, dom.EventType.KEY_UP, (e: KeyboardEvent) => { + this._register(dom.addDisposableListener(this.selectElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { const event = new StandardKeyboardEvent(e); let showDropDown = false; @@ -233,7 +232,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi if (showDropDown) { this.showSelectDropDown(); - dom.EventHelper.stop(e, true); + dom.EventHelper.stop(e); } })); } diff --git a/src/vs/base/browser/ui/splitview/paneview.ts b/src/vs/base/browser/ui/splitview/paneview.ts index 292764c8807..fa034af58f2 100644 --- a/src/vs/base/browser/ui/splitview/paneview.ts +++ b/src/vs/base/browser/ui/splitview/paneview.ts @@ -24,6 +24,7 @@ export interface IPaneOptions { expanded?: boolean; orientation?: Orientation; title: string; + titleDescription?: string; } export interface IPaneStyles { diff --git a/src/vs/base/browser/ui/toolbar/toolbar.ts b/src/vs/base/browser/ui/toolbar/toolbar.ts index 4210cded15c..d20d6d96b53 100644 --- a/src/vs/base/browser/ui/toolbar/toolbar.ts +++ b/src/vs/base/browser/ui/toolbar/toolbar.ts @@ -26,6 +26,7 @@ export interface IToolBarOptions { actionRunner?: IActionRunner; toggleMenuTitle?: string; anchorAlignmentProvider?: () => AnchorAlignment; + renderDropdownAsChildElement?: boolean; } /** @@ -73,7 +74,7 @@ export class ToolBar extends Disposable { keybindingProvider: this.options.getKeyBinding, classNames: toolBarMoreIcon.classNames, anchorAlignmentProvider: this.options.anchorAlignmentProvider, - menuAsChild: true + menuAsChild: !!this.options.renderDropdownAsChildElement } ); this.toggleMenuActionViewItem.setActionContext(this.actionBar.context); @@ -91,10 +92,9 @@ export class ToolBar extends Disposable { } if (action instanceof SubmenuAction) { - const actions = Array.isArray(action.actions) ? action.actions : action.actions(); const result = new DropdownMenuActionViewItem( action, - actions, + action.actions, contextMenuProvider, { actionViewItemProvider: this.options.actionViewItemProvider, diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index be82f6156e2..16e17fcf763 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -881,7 +881,7 @@ class TypeFilterController implements IDisposable { this.messageDomNode.textContent = localize('empty', "No elements found"); this._empty = true; } else { - this.messageDomNode.innerHTML = ''; + this.messageDomNode.innerText = ''; this._empty = false; } diff --git a/src/vs/base/common/actions.ts b/src/vs/base/common/actions.ts index a3b396524eb..196aeda3945 100644 --- a/src/vs/base/common/actions.ts +++ b/src/vs/base/common/actions.ts @@ -237,7 +237,12 @@ export class Separator extends Action { export type SubmenuActions = IAction[] | (() => IAction[]); export class SubmenuAction extends Action { - constructor(id: string, label: string, readonly actions: SubmenuActions, cssClass?: string) { + + get actions(): IAction[] { + return Array.isArray(this._actions) ? this._actions : this._actions(); + } + + constructor(id: string, label: string, private _actions: SubmenuActions, cssClass?: string) { super(id, label, cssClass, true); } } diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index cf9786a2076..48d9454ebf2 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -590,14 +590,6 @@ export function asArray(x: T | T[]): T[] { return Array.isArray(x) ? x : [x]; } -export function toArray(iterable: IterableIterator): T[] { - const result: T[] = []; - for (let element of iterable) { - result.push(element); - } - return result; -} - export function getRandomElement(arr: T[]): T | undefined { return arr[Math.floor(Math.random() * arr.length)]; } diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 6925d74b686..bb39088be12 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -170,6 +170,25 @@ export class Sequencer { } } +export class SequencerByKey { + + private promiseMap = new Map>(); + + queue(key: TKey, promiseTask: ITask>): Promise { + const runningPromise = this.promiseMap.get(key) ?? Promise.resolve(); + const newPromise = runningPromise + .catch(() => { }) + .then(promiseTask) + .finally(() => { + if (this.promiseMap.get(key) === newPromise) { + this.promiseMap.delete(key); + } + }); + this.promiseMap.set(key, newPromise); + return newPromise; + } +} + /** * A helper to delay execution of a task that is being requested often. * diff --git a/src/vs/base/common/comparers.ts b/src/vs/base/common/comparers.ts index a25e2bb9c16..7b45ce5abb6 100644 --- a/src/vs/base/common/comparers.ts +++ b/src/vs/base/common/comparers.ts @@ -33,8 +33,7 @@ const intlFileNameCollatorNumericCaseInsenstive: IdleValue<{ collator: Intl.Coll return { collator: collator }; -}); - +});/** Compares filenames without distinguishing the name from the extension. Disambiguates by unicode comparison. */ export function compareFileNames(one: string | null, other: string | null, caseSensitive = false): number { const a = one || ''; const b = other || ''; @@ -49,36 +48,16 @@ export function compareFileNames(one: string | null, other: string | null, caseS return result; } -/** Compares filenames by name then extension, sorting numbers numerically instead of alphabetically. */ -export function compareFileNamesNumeric(one: string | null, other: string | null): number { - const [oneName, oneExtension] = extractNameAndExtension(one, true); - const [otherName, otherExtension] = extractNameAndExtension(other, true); +/** Compares filenames without distinguishing the name from the extension. Disambiguates by length, not unicode comparison. */ +export function compareFileNamesDefault(one: string | null, other: string | null): number { const collatorNumeric = intlFileNameCollatorNumeric.value.collator; - const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator; - let result; + one = one || ''; + other = other || ''; - // Check for name differences, comparing numbers numerically instead of alphabetically. - result = compareAndDisambiguateByLength(collatorNumeric, oneName, otherName); - if (result !== 0) { - return result; - } - - // Check for case insensitive extension differences, comparing numbers numerically instead of alphabetically. - result = compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension); - if (result !== 0) { - return result; - } - - // Disambiguate the extension case if needed. - if (oneExtension !== otherExtension) { - return collatorNumeric.compare(oneExtension, otherExtension); - } - - return 0; + // Compare the entire filename - both name and extension - and disambiguate by length if needed + return compareAndDisambiguateByLength(collatorNumeric, one, other); } -const FileNameMatch = /^(.*?)(\.([^.]*))?$/; - export function noIntlCompareFileNames(one: string | null, other: string | null, caseSensitive = false): number { if (!caseSensitive) { one = one && one.toLowerCase(); @@ -123,10 +102,12 @@ export function compareFileExtensions(one: string | null, other: string | null): return result; } -/** Compares filenames by extenson, then by name. Sorts numbers numerically, not alphabetically. */ -export function compareFileExtensionsNumeric(one: string | null, other: string | null): number { - const [oneName, oneExtension] = extractNameAndExtension(one, true); - const [otherName, otherExtension] = extractNameAndExtension(other, true); +/** Compares filenames by extenson, then by full filename */ +export function compareFileExtensionsDefault(one: string | null, other: string | null): number { + one = one || ''; + other = other || ''; + const oneExtension = extractExtension(one); + const otherExtension = extractExtension(other); const collatorNumeric = intlFileNameCollatorNumeric.value.collator; const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator; let result; @@ -137,20 +118,12 @@ export function compareFileExtensionsNumeric(one: string | null, other: string | return result; } - // Compare names. - result = compareAndDisambiguateByLength(collatorNumeric, oneName, otherName); - if (result !== 0) { - return result; - } - - // Disambiguate extension case if needed. - if (oneExtension !== otherExtension) { - return collatorNumeric.compare(oneExtension, otherExtension); - } - - return 0; + // Compare full filenames + return compareAndDisambiguateByLength(collatorNumeric, one, other); } +const FileNameMatch = /^(.*?)(\.([^.]*))?$/; + /** Extracts the name and extension from a full filename, with optional special handling for dotfiles */ function extractNameAndExtension(str?: string | null, dotfilesAsNames = false): [string, string] { const match = str ? FileNameMatch.exec(str) as Array : ([] as Array); @@ -166,6 +139,13 @@ function extractNameAndExtension(str?: string | null, dotfilesAsNames = false): return result; } +/** Extracts the extension from a full filename. Treats dotfiles as names, not extensions. */ +function extractExtension(str?: string | null): string { + const match = str ? FileNameMatch.exec(str) as Array : ([] as Array); + + return (match && match[1] && match[1].charAt(0) !== '.' && match[3]) || ''; +} + function compareAndDisambiguateByLength(collator: Intl.Collator, one: string, other: string) { // Check for differences let result = collator.compare(one, other); diff --git a/src/vs/base/common/extpath.ts b/src/vs/base/common/extpath.ts index d6e7d6ef5d7..05d343a5e1b 100644 --- a/src/vs/base/common/extpath.ts +++ b/src/vs/base/common/extpath.ts @@ -142,7 +142,7 @@ export function isUNC(path: string): boolean { // Reference: https://en.wikipedia.org/wiki/Filename const WINDOWS_INVALID_FILE_CHARS = /[\\/:\*\?"<>\|]/g; const UNIX_INVALID_FILE_CHARS = /[\\/]/g; -const WINDOWS_FORBIDDEN_NAMES = /^(con|prn|aux|clock\$|nul|lpt[0-9]|com[0-9])$/i; +const WINDOWS_FORBIDDEN_NAMES = /^(con|prn|aux|clock\$|nul|lpt[0-9]|com[0-9])(\.(.*?))?$/i; export function isValidBasename(name: string | null | undefined, isWindowsOS: boolean = isWindows): boolean { const invalidFileChars = isWindowsOS ? WINDOWS_INVALID_FILE_CHARS : UNIX_INVALID_FILE_CHARS; diff --git a/src/vs/base/common/fuzzyScorer.ts b/src/vs/base/common/fuzzyScorer.ts index 9a4f7bc51bc..11fc4bc0ed3 100644 --- a/src/vs/base/common/fuzzyScorer.ts +++ b/src/vs/base/common/fuzzyScorer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { compareAnything } from 'vs/base/common/comparers'; -import { matchesPrefix, IMatch, matchesCamelCase, isUpper, fuzzyScore, createMatches as createFuzzyMatches, matchesStrictPrefix } from 'vs/base/common/filters'; +import { matchesPrefix, IMatch, isUpper, fuzzyScore, createMatches as createFuzzyMatches } from 'vs/base/common/filters'; import { sep } from 'vs/base/common/path'; import { isWindows, isLinux } from 'vs/base/common/platform'; import { stripWildcards, equalsIgnoreCase } from 'vs/base/common/strings'; @@ -168,7 +168,7 @@ function computeCharScore(queryCharAtIndex: string, queryLowerCharAtIndex: strin score += 1; // if (DEBUG) { - // console.groupCollapsed(`%cCharacter match bonus: +1 (char: ${queryLower[queryIndex]} at index ${targetIndex}, total score: ${score})`, 'font-weight: normal'); + // console.groupCollapsed(`%cCharacter match bonus: +1 (char: ${queryLowerCharAtIndex} at index ${targetIndex}, total score: ${score})`, 'font-weight: normal'); // } // Consecutive match bonus @@ -176,7 +176,7 @@ function computeCharScore(queryCharAtIndex: string, queryLowerCharAtIndex: strin score += (matchesSequenceLength * 5); // if (DEBUG) { - // console.log('Consecutive match bonus: ' + (matchesSequenceLength * 5)); + // console.log(`Consecutive match bonus: +${matchesSequenceLength * 5}`); // } } @@ -206,16 +206,16 @@ function computeCharScore(queryCharAtIndex: string, queryLowerCharAtIndex: strin score += separatorBonus; // if (DEBUG) { - // console.log('After separtor bonus: +4'); + // console.log(`After separtor bonus: +${separatorBonus}`); // } } // Inside word upper case bonus (camel case) else if (isUpper(target.charCodeAt(targetIndex))) { - score += 1; + score += 2; // if (DEBUG) { - // console.log('Inside word upper case bonus: +1'); + // console.log('Inside word upper case bonus: +2'); // } } } @@ -369,10 +369,8 @@ export interface IItemAccessor { } const PATH_IDENTITY_SCORE = 1 << 18; -const LABEL_PREFIX_SCORE_MATCHCASE = 1 << 17; -const LABEL_PREFIX_SCORE_IGNORECASE = 1 << 16; -const LABEL_CAMELCASE_SCORE = 1 << 15; -const LABEL_SCORE_THRESHOLD = 1 << 14; +const LABEL_PREFIX_SCORE_THRESHOLD = 1 << 17; +const LABEL_SCORE_THRESHOLD = 1 << 16; export function scoreItemFuzzy(item: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor, cache: FuzzyScorerCache): IItemScore { if (!item || !query.normalized) { @@ -386,11 +384,17 @@ export function scoreItemFuzzy(item: T, query: IPreparedQuery, fuzzy: boolean const description = accessor.getItemDescription(item); + // in order to speed up scoring, we cache the score with a unique hash based on: + // - label + // - description (if provided) + // - query (normalized) + // - number of query pieces (i.e. 'hello world' and 'helloworld' are different) + // - wether fuzzy matching is enabled or not let cacheHash: string; if (description) { - cacheHash = `${label}${description}${query.normalized}${fuzzy}`; + cacheHash = `${label}${description}${query.normalized}${Array.isArray(query.values) ? query.values.length : ''}${fuzzy}`; } else { - cacheHash = `${label}${query.normalized}${fuzzy}`; + cacheHash = `${label}${query.normalized}${Array.isArray(query.values) ? query.values.length : ''}${fuzzy}`; } const cached = cache[cacheHash]; @@ -455,26 +459,33 @@ function doScoreItemFuzzyMultiple(label: string, description: string | undefined function doScoreItemFuzzySingle(label: string, description: string | undefined, path: string | undefined, query: IPreparedQueryPiece, preferLabelMatches: boolean, fuzzy: boolean): IItemScore { - // Prefer label matches if told so - if (preferLabelMatches) { - - // Treat prefix matches on the label highest - const prefixLabelMatchIgnoreCase = matchesPrefix(query.normalized, label); - if (prefixLabelMatchIgnoreCase) { - const prefixLabelMatchStrictCase = matchesStrictPrefix(query.normalized, label); - return { score: prefixLabelMatchStrictCase ? LABEL_PREFIX_SCORE_MATCHCASE : LABEL_PREFIX_SCORE_IGNORECASE, labelMatch: prefixLabelMatchStrictCase || prefixLabelMatchIgnoreCase }; - } - - // Treat camelcase matches on the label second highest - const camelcaseLabelMatch = matchesCamelCase(query.normalized, label); - if (camelcaseLabelMatch) { - return { score: LABEL_CAMELCASE_SCORE, labelMatch: camelcaseLabelMatch }; - } - - // Prefer scores on the label if any + // Prefer label matches if told so or we have no description + if (preferLabelMatches || !description) { const [labelScore, labelPositions] = scoreFuzzy(label, query.normalized, query.normalizedLowercase, fuzzy); if (labelScore) { - return { score: labelScore + LABEL_SCORE_THRESHOLD, labelMatch: createMatches(labelPositions) }; + + // If we have a prefix match on the label, we give a much + // higher baseScore to elevate these matches over others + // This ensures that typing a file name wins over results + // that are present somewhere in the label, but not the + // beginning. + const labelPrefixMatch = matchesPrefix(query.normalized, label); + let baseScore: number; + if (labelPrefixMatch) { + baseScore = LABEL_PREFIX_SCORE_THRESHOLD; + + // We give another boost to labels that are short, e.g. given + // files "window.ts" and "windowActions.ts" and a query of + // "window", we want "window.ts" to receive a higher score. + // As such we compute the percentage the query has within the + // label and add that to the baseScore. + const prefixLengthBoost = Math.round((query.normalized.length / label.length) * 100); + baseScore += prefixLengthBoost; + } else { + baseScore = LABEL_SCORE_THRESHOLD; + } + + return { score: baseScore + labelScore, labelMatch: labelPrefixMatch || createMatches(labelPositions) }; } } @@ -594,81 +605,42 @@ export function compareItemsByFuzzyScore(itemA: T, itemB: T, query: IPrepared const scoreA = itemScoreA.score; const scoreB = itemScoreB.score; - // 1.) prefer identity matches + // 1.) identity matches have highest score if (scoreA === PATH_IDENTITY_SCORE || scoreB === PATH_IDENTITY_SCORE) { if (scoreA !== scoreB) { return scoreA === PATH_IDENTITY_SCORE ? -1 : 1; } } - // 2.) prefer label prefix matches (match case) - if (scoreA === LABEL_PREFIX_SCORE_MATCHCASE || scoreB === LABEL_PREFIX_SCORE_MATCHCASE) { - if (scoreA !== scoreB) { - return scoreA === LABEL_PREFIX_SCORE_MATCHCASE ? -1 : 1; - } - - const labelA = accessor.getItemLabel(itemA) || ''; - const labelB = accessor.getItemLabel(itemB) || ''; - - // prefer shorter names when both match on label prefix - if (labelA.length !== labelB.length) { - return labelA.length - labelB.length; - } - } - - // 3.) prefer label prefix matches (ignore case) - if (scoreA === LABEL_PREFIX_SCORE_IGNORECASE || scoreB === LABEL_PREFIX_SCORE_IGNORECASE) { - if (scoreA !== scoreB) { - return scoreA === LABEL_PREFIX_SCORE_IGNORECASE ? -1 : 1; - } - - const labelA = accessor.getItemLabel(itemA) || ''; - const labelB = accessor.getItemLabel(itemB) || ''; - - // prefer shorter names when both match on label prefix - if (labelA.length !== labelB.length) { - return labelA.length - labelB.length; - } - } - - // 4.) prefer camelcase matches - if (scoreA === LABEL_CAMELCASE_SCORE || scoreB === LABEL_CAMELCASE_SCORE) { - if (scoreA !== scoreB) { - return scoreA === LABEL_CAMELCASE_SCORE ? -1 : 1; - } - - const labelA = accessor.getItemLabel(itemA) || ''; - const labelB = accessor.getItemLabel(itemB) || ''; - - // prefer more compact camel case matches over longer - const comparedByMatchLength = compareByMatchLength(itemScoreA.labelMatch, itemScoreB.labelMatch); - if (comparedByMatchLength !== 0) { - return comparedByMatchLength; - } - - // prefer shorter names when both match on label camelcase - if (labelA.length !== labelB.length) { - return labelA.length - labelB.length; - } - } - - // 5.) prefer label scores + // 2.) matches on label are considered higher compared to label+description matches if (scoreA > LABEL_SCORE_THRESHOLD || scoreB > LABEL_SCORE_THRESHOLD) { - if (scoreB < LABEL_SCORE_THRESHOLD) { - return -1; + if (scoreA !== scoreB) { + return scoreA > scoreB ? -1 : 1; } - if (scoreA < LABEL_SCORE_THRESHOLD) { - return 1; + // prefer more compact matches over longer in label (unless this is a prefix match where + // longer prefix matches are actually preferred) + if (scoreA < LABEL_PREFIX_SCORE_THRESHOLD && scoreB < LABEL_PREFIX_SCORE_THRESHOLD) { + const comparedByMatchLength = compareByMatchLength(itemScoreA.labelMatch, itemScoreB.labelMatch); + if (comparedByMatchLength !== 0) { + return comparedByMatchLength; + } + } + + // prefer shorter labels over longer labels + const labelA = accessor.getItemLabel(itemA) || ''; + const labelB = accessor.getItemLabel(itemB) || ''; + if (labelA.length !== labelB.length) { + return labelA.length - labelB.length; } } - // 6.) compare by score + // 3.) compare by score in label+description if (scoreA !== scoreB) { return scoreA > scoreB ? -1 : 1; } - // 7.) prefer matches in label over non-label matches + // 4.) scores are identical: prefer matches in label over non-label matches const itemAHasLabelMatches = Array.isArray(itemScoreA.labelMatch) && itemScoreA.labelMatch.length > 0; const itemBHasLabelMatches = Array.isArray(itemScoreB.labelMatch) && itemScoreB.labelMatch.length > 0; if (itemAHasLabelMatches && !itemBHasLabelMatches) { @@ -677,15 +649,14 @@ export function compareItemsByFuzzyScore(itemA: T, itemB: T, query: IPrepared return 1; } - // 8.) scores are identical, prefer more compact matches (label and description) + // 5.) scores are identical: prefer more compact matches (label and description) const itemAMatchDistance = computeLabelAndDescriptionMatchDistance(itemA, itemScoreA, accessor); const itemBMatchDistance = computeLabelAndDescriptionMatchDistance(itemB, itemScoreB, accessor); if (itemAMatchDistance && itemBMatchDistance && itemAMatchDistance !== itemBMatchDistance) { return itemBMatchDistance > itemAMatchDistance ? -1 : 1; } - // 9.) at this point, scores are identical and match compactness as well - // for both items so we start to use the fallback compare + // 6.) scores are identical: start to use the fallback compare return fallbackCompare(itemA, itemB, query, accessor); } diff --git a/src/vs/base/common/hash.ts b/src/vs/base/common/hash.ts index 4b47073d8e5..d5770516836 100644 --- a/src/vs/base/common/hash.ts +++ b/src/vs/base/common/hash.ts @@ -8,7 +8,12 @@ import * as strings from 'vs/base/common/strings'; /** * Return a hash value for an object. */ -export function hash(obj: any, hashVal = 0): number { +export function hash(obj: any): number { + return doHash(obj, 0); +} + + +export function doHash(obj: any, hashVal: number): number { switch (typeof obj) { case 'object': if (obj === null) { @@ -24,9 +29,9 @@ export function hash(obj: any, hashVal = 0): number { case 'number': return numberHash(obj, hashVal); case 'undefined': - return numberHash(0, 937); + return numberHash(937, hashVal); default: - return numberHash(0, 617); + return numberHash(617, hashVal); } } @@ -48,14 +53,14 @@ export function stringHash(s: string, hashVal: number) { function arrayHash(arr: any[], initialHashVal: number): number { initialHashVal = numberHash(104579, initialHashVal); - return arr.reduce((hashVal, item) => hash(item, hashVal), initialHashVal); + return arr.reduce((hashVal, item) => doHash(item, hashVal), initialHashVal); } function objectHash(obj: any, initialHashVal: number): number { initialHashVal = numberHash(181387, initialHashVal); return Object.keys(obj).sort().reduce((hashVal, key) => { hashVal = stringHash(key, hashVal); - return hash(obj[key], hashVal); + return doHash(obj[key], hashVal); }, initialHashVal); } @@ -68,7 +73,7 @@ export class Hasher { } hash(obj: any): number { - this._value = hash(obj, this._value); + this._value = doHash(obj, this._value); return this._value; } } diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index 406a9c801d4..79bbb910aac 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -45,6 +45,14 @@ function trackDisposable(x: T): T { return x; } +export class MultiDisposeError extends Error { + constructor( + public readonly errors: any[] + ) { + super(`Encounter errors while disposing of store. Errors: [${errors.join(', ')}]`); + } +} + export interface IDisposable { dispose(): void; } @@ -60,12 +68,25 @@ export function dispose(disposables: Array): Array; export function dispose(disposables: ReadonlyArray): ReadonlyArray; export function dispose(arg: T | IterableIterator | undefined): any { if (Iterable.is(arg)) { - for (let d of arg) { + let errors: any[] = []; + + for (const d of arg) { if (d) { markTracked(d); - d.dispose(); + try { + d.dispose(); + } catch (e) { + errors.push(e); + } } } + + if (errors.length === 1) { + throw errors[0]; + } else if (errors.length > 1) { + throw new MultiDisposeError(errors); + } + return Array.isArray(arg) ? [] : arg; } else if (arg) { markTracked(arg); @@ -116,8 +137,11 @@ export class DisposableStore implements IDisposable { * Dispose of all registered disposables but do not mark this object as disposed. */ public clear(): void { - this._toDispose.forEach(item => item.dispose()); - this._toDispose.clear(); + try { + dispose(this._toDispose.values()); + } finally { + this._toDispose.clear(); + } } public add(t: T): T { diff --git a/src/vs/base/common/marked/marked.js b/src/vs/base/common/marked/marked.js index 1288f459647..fb133b90fb8 100644 --- a/src/vs/base/common/marked/marked.js +++ b/src/vs/base/common/marked/marked.js @@ -1,1714 +1,2627 @@ /** * marked - a markdown parser - * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) + * Copyright (c) 2011-2020, Christopher Jeffrey. (MIT Licensed) * https://github.com/markedjs/marked */ -// BEGIN MONACOCHANGE -var __marked_exports; -// END MONACOCHANGE - -;(function(root) { -'use strict'; - /** - * Block-Level Grammar + * DO NOT EDIT THIS FILE + * The code in this file is generated from files in ./src/ */ -var block = { - newline: /^\n+/, - code: /^( {4}[^\n]+\n*)+/, - fences: noop, - hr: /^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/, - heading: /^ *(#{1,6}) *([^\n]+?) *(?:#+ *)?(?:\n+|$)/, - nptable: noop, - blockquote: /^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/, - list: /^( {0,3})(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, - html: '^ {0,3}(?:' // optional indentation - + '<(script|pre|style)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)' // (1) - + '|comment[^\\n]*(\\n+|$)' // (2) - + '|<\\?[\\s\\S]*?\\?>\\n*' // (3) - + '|\\n*' // (4) - + '|\\n*' // (5) - + '|)[\\s\\S]*?(?:\\n{2,}|$)' // (6) - + '|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$)' // (7) open tag - + '|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$)' // (7) closing tag - + ')', - def: /^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/, - table: noop, - lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/, - paragraph: /^([^\n]+(?:\n(?!hr|heading|lheading| {0,3}>|<\/?(?:tag)(?: +|\n|\/?>)|<(?:script|pre|style|!--))[^\n]+)*)/, - text: /^[^\n]+/ -}; +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, global.marked = factory()); +}(this, (function () { 'use strict'; -block._label = /(?!\s*\])(?:\\[\[\]]|[^\[\]])+/; -block._title = /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/; -block.def = edit(block.def) - .replace('label', block._label) - .replace('title', block._title) - .getRegex(); - -block.bullet = /(?:[*+-]|\d{1,9}\.)/; -block.item = /^( *)(bull) ?[^\n]*(?:\n(?!\1bull ?)[^\n]*)*/; -block.item = edit(block.item, 'gm') - .replace(/bull/g, block.bullet) - .getRegex(); - -block.list = edit(block.list) - .replace(/bull/g, block.bullet) - .replace('hr', '\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))') - .replace('def', '\\n+(?=' + block.def.source + ')') - .getRegex(); - -block._tag = 'address|article|aside|base|basefont|blockquote|body|caption' - + '|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption' - + '|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe' - + '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option' - + '|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr' - + '|track|ul'; -block._comment = //; -block.html = edit(block.html, 'i') - .replace('comment', block._comment) - .replace('tag', block._tag) - .replace('attribute', / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/) - .getRegex(); - -block.paragraph = edit(block.paragraph) - .replace('hr', block.hr) - .replace('heading', block.heading) - .replace('lheading', block.lheading) - .replace('tag', block._tag) // pars can be interrupted by type (6) html blocks - .getRegex(); - -block.blockquote = edit(block.blockquote) - .replace('paragraph', block.paragraph) - .getRegex(); - -/** - * Normal Block Grammar - */ - -block.normal = merge({}, block); - -/** - * GFM Block Grammar - */ - -block.gfm = merge({}, block.normal, { - fences: /^ {0,3}(`{3,}|~{3,})([^`\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?:\n+|$)|$)/, - paragraph: /^/, - heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/ -}); - -block.gfm.paragraph = edit(block.paragraph) - .replace('(?!', '(?!' - + block.gfm.fences.source.replace('\\1', '\\2') + '|' - + block.list.source.replace('\\1', '\\3') + '|') - .getRegex(); - -/** - * GFM + Tables Block Grammar - */ - -block.tables = merge({}, block.gfm, { - nptable: /^ *([^|\n ].*\|.*)\n *([-:]+ *\|[-| :]*)(?:\n((?:.*[^>\n ].*(?:\n|$))*)\n*|$)/, - table: /^ *\|(.+)\n *\|?( *[-:]+[-| :]*)(?:\n((?: *[^>\n ].*(?:\n|$))*)\n*|$)/ -}); - -/** - * Pedantic grammar - */ - -block.pedantic = merge({}, block.normal, { - html: edit( - '^ *(?:comment *(?:\\n|\\s*$)' - + '|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)' // closed tag - + '|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))') - .replace('comment', block._comment) - .replace(/tag/g, '(?!(?:' - + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub' - + '|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)' - + '\\b)\\w+(?!:|[^\\w\\s@]*@)\\b') - .getRegex(), - def: /^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/ -}); - -/** - * Block Lexer - */ - -function Lexer(options) { - this.tokens = []; - this.tokens.links = Object.create(null); - this.options = options || marked.defaults; - this.rules = block.normal; - - if (this.options.pedantic) { - this.rules = block.pedantic; - } else if (this.options.gfm) { - if (this.options.tables) { - this.rules = block.tables; - } else { - this.rules = block.gfm; + function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); } } -} -/** - * Expose Block Rules - */ + function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; + } -Lexer.rules = block; + function _unsupportedIterableToArray(o, minLen) { + if (!o) return; + if (typeof o === "string") return _arrayLikeToArray(o, minLen); + var n = Object.prototype.toString.call(o).slice(8, -1); + if (n === "Object" && o.constructor) n = o.constructor.name; + if (n === "Map" || n === "Set") return Array.from(o); + if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); + } -/** - * Static Lex Method - */ + function _arrayLikeToArray(arr, len) { + if (len == null || len > arr.length) len = arr.length; -Lexer.lex = function(src, options) { - var lexer = new Lexer(options); - return lexer.lex(src); -}; + for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; -/** - * Preprocessing - */ + return arr2; + } -Lexer.prototype.lex = function(src) { - src = src - .replace(/\r\n|\r/g, '\n') - .replace(/\t/g, ' ') - .replace(/\u00a0/g, ' ') - .replace(/\u2424/g, '\n'); + function _createForOfIteratorHelperLoose(o, allowArrayLike) { + var it; - return this.token(src, true); -}; + if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { + if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { + if (it) o = it; + var i = 0; + return function () { + if (i >= o.length) return { + done: true + }; + return { + done: false, + value: o[i++] + }; + }; + } -/** - * Lexing - */ + throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); + } -Lexer.prototype.token = function(src, top) { - src = src.replace(/^ +$/gm, ''); - var next, - loose, - cap, - bull, - b, - item, - listStart, - listItems, - t, - space, - i, - tag, - l, - isordered, - istask, - ischecked; + it = o[Symbol.iterator](); + return it.next.bind(it); + } - while (src) { - // newline - if (cap = this.rules.newline.exec(src)) { - src = src.substring(cap[0].length); - if (cap[0].length > 1) { - this.tokens.push({ - type: 'space' - }); + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } + + var defaults = createCommonjsModule(function (module) { + function getDefaults() { + return { + baseUrl: null, + breaks: false, + gfm: true, + headerIds: true, + headerPrefix: '', + highlight: null, + langPrefix: 'language-', + mangle: true, + pedantic: false, + renderer: null, + sanitize: false, + sanitizer: null, + silent: false, + smartLists: false, + smartypants: false, + tokenizer: null, + walkTokens: null, + xhtml: false + }; + } + + function changeDefaults(newDefaults) { + module.exports.defaults = newDefaults; + } + + module.exports = { + defaults: getDefaults(), + getDefaults: getDefaults, + changeDefaults: changeDefaults + }; + }); + var defaults_1 = defaults.defaults; + var defaults_2 = defaults.getDefaults; + var defaults_3 = defaults.changeDefaults; + + /** + * Helpers + */ + var escapeTest = /[&<>"']/; + var escapeReplace = /[&<>"']/g; + var escapeTestNoEncode = /[<>"']|&(?!#?\w+;)/; + var escapeReplaceNoEncode = /[<>"']|&(?!#?\w+;)/g; + var escapeReplacements = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + + var getEscapeReplacement = function getEscapeReplacement(ch) { + return escapeReplacements[ch]; + }; + + function escape(html, encode) { + if (encode) { + if (escapeTest.test(html)) { + return html.replace(escapeReplace, getEscapeReplacement); + } + } else { + if (escapeTestNoEncode.test(html)) { + return html.replace(escapeReplaceNoEncode, getEscapeReplacement); } } - // code - if (cap = this.rules.code.exec(src)) { - src = src.substring(cap[0].length); - cap = cap[0].replace(/^ {4}/gm, ''); - this.tokens.push({ - type: 'code', - text: !this.options.pedantic - ? rtrim(cap, '\n') - : cap - }); - continue; - } + return html; + } - // fences (gfm) - if (cap = this.rules.fences.exec(src)) { - src = src.substring(cap[0].length); - this.tokens.push({ - type: 'code', - lang: cap[2] ? cap[2].trim() : cap[2], - text: cap[3] || '' - }); - continue; - } + var unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig; - // heading - if (cap = this.rules.heading.exec(src)) { - src = src.substring(cap[0].length); - this.tokens.push({ - type: 'heading', - depth: cap[1].length, - text: cap[2] - }); - continue; - } + function unescape(html) { + // explicitly match decimal, hex, and named HTML entities + return html.replace(unescapeTest, function (_, n) { + n = n.toLowerCase(); + if (n === 'colon') return ':'; - // table no leading pipe (gfm) - if (cap = this.rules.nptable.exec(src)) { - item = { - type: 'table', - header: splitCells(cap[1].replace(/^ *| *\| *$/g, '')), - align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), - cells: cap[3] ? cap[3].replace(/\n$/, '').split('\n') : [] - }; + if (n.charAt(0) === '#') { + return n.charAt(1) === 'x' ? String.fromCharCode(parseInt(n.substring(2), 16)) : String.fromCharCode(+n.substring(1)); + } - if (item.header.length === item.align.length) { - src = src.substring(cap[0].length); + return ''; + }); + } - for (i = 0; i < item.align.length; i++) { - if (/^ *-+: *$/.test(item.align[i])) { - item.align[i] = 'right'; - } else if (/^ *:-+: *$/.test(item.align[i])) { - item.align[i] = 'center'; - } else if (/^ *:-+ *$/.test(item.align[i])) { - item.align[i] = 'left'; - } else { - item.align[i] = null; - } - } + var caret = /(^|[^\[])\^/g; - for (i = 0; i < item.cells.length; i++) { - item.cells[i] = splitCells(item.cells[i], item.header.length); - } + function edit(regex, opt) { + regex = regex.source || regex; + opt = opt || ''; + var obj = { + replace: function replace(name, val) { + val = val.source || val; + val = val.replace(caret, '$1'); + regex = regex.replace(name, val); + return obj; + }, + getRegex: function getRegex() { + return new RegExp(regex, opt); + } + }; + return obj; + } - this.tokens.push(item); + var nonWordAndColonTest = /[^\w:]/g; + var originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i; - continue; + function cleanUrl(sanitize, base, href) { + if (sanitize) { + var prot; + + try { + prot = decodeURIComponent(unescape(href)).replace(nonWordAndColonTest, '').toLowerCase(); + } catch (e) { + return null; + } + + if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) { + return null; } } - // hr - if (cap = this.rules.hr.exec(src)) { - src = src.substring(cap[0].length); - this.tokens.push({ - type: 'hr' - }); - continue; + if (base && !originIndependentUrl.test(href)) { + href = resolveUrl(base, href); } - // blockquote - if (cap = this.rules.blockquote.exec(src)) { - src = src.substring(cap[0].length); - - this.tokens.push({ - type: 'blockquote_start' - }); - - cap = cap[0].replace(/^ *> ?/gm, ''); - - // Pass `top` to keep the current - // "toplevel" state. This is exactly - // how markdown.pl works. - this.token(cap, top); - - this.tokens.push({ - type: 'blockquote_end' - }); - - continue; + try { + href = encodeURI(href).replace(/%25/g, '%'); + } catch (e) { + return null; } - // list - if (cap = this.rules.list.exec(src)) { - src = src.substring(cap[0].length); - bull = cap[2]; - isordered = bull.length > 1; + return href; + } - listStart = { - type: 'list_start', - ordered: isordered, - start: isordered ? +bull : '', - loose: false + var baseUrls = {}; + var justDomain = /^[^:]+:\/*[^/]*$/; + var protocol = /^([^:]+:)[\s\S]*$/; + var domain = /^([^:]+:\/*[^/]*)[\s\S]*$/; + + function resolveUrl(base, href) { + if (!baseUrls[' ' + base]) { + // we can ignore everything in base after the last slash of its path component, + // but we might need to add _that_ + // https://tools.ietf.org/html/rfc3986#section-3 + if (justDomain.test(base)) { + baseUrls[' ' + base] = base + '/'; + } else { + baseUrls[' ' + base] = rtrim(base, '/', true); + } + } + + base = baseUrls[' ' + base]; + var relativeBase = base.indexOf(':') === -1; + + if (href.substring(0, 2) === '//') { + if (relativeBase) { + return href; + } + + return base.replace(protocol, '$1') + href; + } else if (href.charAt(0) === '/') { + if (relativeBase) { + return href; + } + + return base.replace(domain, '$1') + href; + } else { + return base + href; + } + } + + var noopTest = { + exec: function noopTest() {} + }; + + function merge(obj) { + var i = 1, + target, + key; + + for (; i < arguments.length; i++) { + target = arguments[i]; + + for (key in target) { + if (Object.prototype.hasOwnProperty.call(target, key)) { + obj[key] = target[key]; + } + } + } + + return obj; + } + + function splitCells(tableRow, count) { + // ensure that every cell-delimiting pipe has a space + // before it to distinguish it from an escaped pipe + var row = tableRow.replace(/\|/g, function (match, offset, str) { + var escaped = false, + curr = offset; + + while (--curr >= 0 && str[curr] === '\\') { + escaped = !escaped; + } + + if (escaped) { + // odd number of slashes means | is escaped + // so we leave it alone + return '|'; + } else { + // add space before unescaped | + return ' |'; + } + }), + cells = row.split(/ \|/); + var i = 0; + + if (cells.length > count) { + cells.splice(count); + } else { + while (cells.length < count) { + cells.push(''); + } + } + + for (; i < cells.length; i++) { + // leading or trailing whitespace is ignored per the gfm spec + cells[i] = cells[i].trim().replace(/\\\|/g, '|'); + } + + return cells; + } // Remove trailing 'c's. Equivalent to str.replace(/c*$/, ''). + // /c*$/ is vulnerable to REDOS. + // invert: Remove suffix of non-c chars instead. Default falsey. + + + function rtrim(str, c, invert) { + var l = str.length; + + if (l === 0) { + return ''; + } // Length of suffix matching the invert condition. + + + var suffLen = 0; // Step left until we fail to match the invert condition. + + while (suffLen < l) { + var currChar = str.charAt(l - suffLen - 1); + + if (currChar === c && !invert) { + suffLen++; + } else if (currChar !== c && invert) { + suffLen++; + } else { + break; + } + } + + return str.substr(0, l - suffLen); + } + + function findClosingBracket(str, b) { + if (str.indexOf(b[1]) === -1) { + return -1; + } + + var l = str.length; + var level = 0, + i = 0; + + for (; i < l; i++) { + if (str[i] === '\\') { + i++; + } else if (str[i] === b[0]) { + level++; + } else if (str[i] === b[1]) { + level--; + + if (level < 0) { + return i; + } + } + } + + return -1; + } + + function checkSanitizeDeprecation(opt) { + if (opt && opt.sanitize && !opt.silent) { + // VS CODE CHANGE + // Disable logging about sanitize options. We already use insane after running the sanitizer + + // console.warn('marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options'); + } + } + + var helpers = { + escape: escape, + unescape: unescape, + edit: edit, + cleanUrl: cleanUrl, + resolveUrl: resolveUrl, + noopTest: noopTest, + merge: merge, + splitCells: splitCells, + rtrim: rtrim, + findClosingBracket: findClosingBracket, + checkSanitizeDeprecation: checkSanitizeDeprecation + }; + + var defaults$1 = defaults.defaults; + var rtrim$1 = helpers.rtrim, + splitCells$1 = helpers.splitCells, + _escape = helpers.escape, + findClosingBracket$1 = helpers.findClosingBracket; + + function outputLink(cap, link, raw) { + var href = link.href; + var title = link.title ? _escape(link.title) : null; + var text = cap[1].replace(/\\([\[\]])/g, '$1'); + + if (cap[0].charAt(0) !== '!') { + return { + type: 'link', + raw: raw, + href: href, + title: title, + text: text }; + } else { + return { + type: 'image', + raw: raw, + href: href, + title: title, + text: _escape(text) + }; + } + } - this.tokens.push(listStart); + function indentCodeCompensation(raw, text) { + var matchIndentToCode = raw.match(/^(\s+)(?:```)/); - // Get each top-level item. - cap = cap[0].match(this.rules.item); + if (matchIndentToCode === null) { + return text; + } - listItems = []; - next = false; - l = cap.length; - i = 0; + var indentToCode = matchIndentToCode[1]; + return text.split('\n').map(function (node) { + var matchIndentInNode = node.match(/^\s+/); - for (; i < l; i++) { - item = cap[i]; + if (matchIndentInNode === null) { + return node; + } - // Remove the list item's bullet - // so it is seen as the next token. - space = item.length; - item = item.replace(/^ *([*+-]|\d+\.) */, ''); + var indentInNode = matchIndentInNode[0]; - // Outdent whatever the - // list item contains. Hacky. - if (~item.indexOf('\n ')) { - space -= item.length; - item = !this.options.pedantic - ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '') - : item.replace(/^ {1,4}/gm, ''); + if (indentInNode.length >= indentToCode.length) { + return node.slice(indentToCode.length); + } + + return node; + }).join('\n'); + } + /** + * Tokenizer + */ + + + var Tokenizer_1 = /*#__PURE__*/function () { + function Tokenizer(options) { + this.options = options || defaults$1; + } + + var _proto = Tokenizer.prototype; + + _proto.space = function space(src) { + var cap = this.rules.block.newline.exec(src); + + if (cap) { + if (cap[0].length > 1) { + return { + type: 'space', + raw: cap[0] + }; } - // Determine whether the next list item belongs here. - // Backpedal if it does not belong in this list. - if (i !== l - 1) { - b = block.bullet.exec(cap[i + 1])[0]; - if (bull.length > 1 ? b.length === 1 - : (b.length > 1 || (this.options.smartLists && b !== bull))) { - src = cap.slice(i + 1).join('\n') + src; - i = l - 1; - } + return { + raw: '\n' + }; + } + }; + + _proto.code = function code(src, tokens) { + var cap = this.rules.block.code.exec(src); + + if (cap) { + var lastToken = tokens[tokens.length - 1]; // An indented code block cannot interrupt a paragraph. + + if (lastToken && lastToken.type === 'paragraph') { + return { + raw: cap[0], + text: cap[0].trimRight() + }; } - // Determine whether item is loose or not. - // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ - // for discount behavior. - loose = next || /\n\n(?!\s*$)/.test(item); - if (i !== l - 1) { - next = item.charAt(item.length - 1) === '\n'; - if (!loose) loose = next; - } + var text = cap[0].replace(/^ {4}/gm, ''); + return { + type: 'code', + raw: cap[0], + codeBlockStyle: 'indented', + text: !this.options.pedantic ? rtrim$1(text, '\n') : text + }; + } + }; - if (loose) { - listStart.loose = true; - } + _proto.fences = function fences(src) { + var cap = this.rules.block.fences.exec(src); - // Check for task list items - istask = /^\[[ xX]\] /.test(item); - ischecked = undefined; - if (istask) { - ischecked = item[1] !== ' '; - item = item.replace(/^\[[ xX]\] +/, ''); - } + if (cap) { + var raw = cap[0]; + var text = indentCodeCompensation(raw, cap[3] || ''); + return { + type: 'code', + raw: raw, + lang: cap[2] ? cap[2].trim() : cap[2], + text: text + }; + } + }; - t = { - type: 'list_item_start', - task: istask, - checked: ischecked, - loose: loose + _proto.heading = function heading(src) { + var cap = this.rules.block.heading.exec(src); + + if (cap) { + return { + type: 'heading', + raw: cap[0], + depth: cap[1].length, + text: cap[2] + }; + } + }; + + _proto.nptable = function nptable(src) { + var cap = this.rules.block.nptable.exec(src); + + if (cap) { + var item = { + type: 'table', + header: splitCells$1(cap[1].replace(/^ *| *\| *$/g, '')), + align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), + cells: cap[3] ? cap[3].replace(/\n$/, '').split('\n') : [], + raw: cap[0] }; - listItems.push(t); - this.tokens.push(t); + if (item.header.length === item.align.length) { + var l = item.align.length; + var i; - // Recurse. - this.token(item, false); + for (i = 0; i < l; i++) { + if (/^ *-+: *$/.test(item.align[i])) { + item.align[i] = 'right'; + } else if (/^ *:-+: *$/.test(item.align[i])) { + item.align[i] = 'center'; + } else if (/^ *:-+ *$/.test(item.align[i])) { + item.align[i] = 'left'; + } else { + item.align[i] = null; + } + } - this.tokens.push({ - type: 'list_item_end' - }); - } + l = item.cells.length; - if (listStart.loose) { - l = listItems.length; - i = 0; - for (; i < l; i++) { - listItems[i].loose = true; + for (i = 0; i < l; i++) { + item.cells[i] = splitCells$1(item.cells[i], item.header.length); + } + + return item; } } + }; - this.tokens.push({ - type: 'list_end' - }); + _proto.hr = function hr(src) { + var cap = this.rules.block.hr.exec(src); - continue; - } + if (cap) { + return { + type: 'hr', + raw: cap[0] + }; + } + }; - // html - if (cap = this.rules.html.exec(src)) { - src = src.substring(cap[0].length); - this.tokens.push({ - type: this.options.sanitize - ? 'paragraph' - : 'html', - pre: !this.options.sanitizer - && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'), - text: cap[0] - }); - continue; - } + _proto.blockquote = function blockquote(src) { + var cap = this.rules.block.blockquote.exec(src); - // def - if (top && (cap = this.rules.def.exec(src))) { - src = src.substring(cap[0].length); - if (cap[3]) cap[3] = cap[3].substring(1, cap[3].length - 1); - tag = cap[1].toLowerCase().replace(/\s+/g, ' '); - if (!this.tokens.links[tag]) { - this.tokens.links[tag] = { + if (cap) { + var text = cap[0].replace(/^ *> ?/gm, ''); + return { + type: 'blockquote', + raw: cap[0], + text: text + }; + } + }; + + _proto.list = function list(src) { + var cap = this.rules.block.list.exec(src); + + if (cap) { + var raw = cap[0]; + var bull = cap[2]; + var isordered = bull.length > 1; + var isparen = bull[bull.length - 1] === ')'; + var list = { + type: 'list', + raw: raw, + ordered: isordered, + start: isordered ? +bull.slice(0, -1) : '', + loose: false, + items: [] + }; // Get each top-level item. + + var itemMatch = cap[0].match(this.rules.block.item); + var next = false, + item, + space, + b, + addBack, + loose, + istask, + ischecked; + var l = itemMatch.length; + + for (var i = 0; i < l; i++) { + item = itemMatch[i]; + raw = item; // Remove the list item's bullet + // so it is seen as the next token. + + space = item.length; + item = item.replace(/^ *([*+-]|\d+[.)]) */, ''); // Outdent whatever the + // list item contains. Hacky. + + if (~item.indexOf('\n ')) { + space -= item.length; + item = !this.options.pedantic ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '') : item.replace(/^ {1,4}/gm, ''); + } // Determine whether the next list item belongs here. + // Backpedal if it does not belong in this list. + + + if (i !== l - 1) { + b = this.rules.block.bullet.exec(itemMatch[i + 1])[0]; + + if (isordered ? b.length === 1 || !isparen && b[b.length - 1] === ')' : b.length > 1 || this.options.smartLists && b !== bull) { + addBack = itemMatch.slice(i + 1).join('\n'); + list.raw = list.raw.substring(0, list.raw.length - addBack.length); + i = l - 1; + } + } // Determine whether item is loose or not. + // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ + // for discount behavior. + + + loose = next || /\n\n(?!\s*$)/.test(item); + + if (i !== l - 1) { + next = item.charAt(item.length - 1) === '\n'; + if (!loose) loose = next; + } + + if (loose) { + list.loose = true; + } // Check for task list items + + + istask = /^\[[ xX]\] /.test(item); + ischecked = undefined; + + if (istask) { + ischecked = item[1] !== ' '; + item = item.replace(/^\[[ xX]\] +/, ''); + } + + list.items.push({ + type: 'list_item', + raw: raw, + task: istask, + checked: ischecked, + loose: loose, + text: item + }); + } + + return list; + } + }; + + _proto.html = function html(src) { + var cap = this.rules.block.html.exec(src); + + if (cap) { + return { + type: this.options.sanitize ? 'paragraph' : 'html', + raw: cap[0], + pre: !this.options.sanitizer && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'), + text: this.options.sanitize ? this.options.sanitizer ? this.options.sanitizer(cap[0]) : _escape(cap[0]) : cap[0] + }; + } + }; + + _proto.def = function def(src) { + var cap = this.rules.block.def.exec(src); + + if (cap) { + if (cap[3]) cap[3] = cap[3].substring(1, cap[3].length - 1); + var tag = cap[1].toLowerCase().replace(/\s+/g, ' '); + return { + tag: tag, + raw: cap[0], href: cap[2], title: cap[3] }; } - continue; - } + }; - // table (gfm) - if (cap = this.rules.table.exec(src)) { - item = { - type: 'table', - header: splitCells(cap[1].replace(/^ *| *\| *$/g, '')), - align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), - cells: cap[3] ? cap[3].replace(/\n$/, '').split('\n') : [] - }; + _proto.table = function table(src) { + var cap = this.rules.block.table.exec(src); - if (item.header.length === item.align.length) { - src = src.substring(cap[0].length); + if (cap) { + var item = { + type: 'table', + header: splitCells$1(cap[1].replace(/^ *| *\| *$/g, '')), + align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), + cells: cap[3] ? cap[3].replace(/\n$/, '').split('\n') : [] + }; - for (i = 0; i < item.align.length; i++) { - if (/^ *-+: *$/.test(item.align[i])) { - item.align[i] = 'right'; - } else if (/^ *:-+: *$/.test(item.align[i])) { - item.align[i] = 'center'; - } else if (/^ *:-+ *$/.test(item.align[i])) { - item.align[i] = 'left'; + if (item.header.length === item.align.length) { + item.raw = cap[0]; + var l = item.align.length; + var i; + + for (i = 0; i < l; i++) { + if (/^ *-+: *$/.test(item.align[i])) { + item.align[i] = 'right'; + } else if (/^ *:-+: *$/.test(item.align[i])) { + item.align[i] = 'center'; + } else if (/^ *:-+ *$/.test(item.align[i])) { + item.align[i] = 'left'; + } else { + item.align[i] = null; + } + } + + l = item.cells.length; + + for (i = 0; i < l; i++) { + item.cells[i] = splitCells$1(item.cells[i].replace(/^ *\| *| *\| *$/g, ''), item.header.length); + } + + return item; + } + } + }; + + _proto.lheading = function lheading(src) { + var cap = this.rules.block.lheading.exec(src); + + if (cap) { + return { + type: 'heading', + raw: cap[0], + depth: cap[2].charAt(0) === '=' ? 1 : 2, + text: cap[1] + }; + } + }; + + _proto.paragraph = function paragraph(src) { + var cap = this.rules.block.paragraph.exec(src); + + if (cap) { + return { + type: 'paragraph', + raw: cap[0], + text: cap[1].charAt(cap[1].length - 1) === '\n' ? cap[1].slice(0, -1) : cap[1] + }; + } + }; + + _proto.text = function text(src, tokens) { + var cap = this.rules.block.text.exec(src); + + if (cap) { + var lastToken = tokens[tokens.length - 1]; + + if (lastToken && lastToken.type === 'text') { + return { + raw: cap[0], + text: cap[0] + }; + } + + return { + type: 'text', + raw: cap[0], + text: cap[0] + }; + } + }; + + _proto.escape = function escape(src) { + var cap = this.rules.inline.escape.exec(src); + + if (cap) { + return { + type: 'escape', + raw: cap[0], + text: _escape(cap[1]) + }; + } + }; + + _proto.tag = function tag(src, inLink, inRawBlock) { + var cap = this.rules.inline.tag.exec(src); + + if (cap) { + if (!inLink && /^/i.test(cap[0])) { + inLink = false; + } + + if (!inRawBlock && /^<(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { + inRawBlock = true; + } else if (inRawBlock && /^<\/(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { + inRawBlock = false; + } + + return { + type: this.options.sanitize ? 'text' : 'html', + raw: cap[0], + inLink: inLink, + inRawBlock: inRawBlock, + text: this.options.sanitize ? this.options.sanitizer ? this.options.sanitizer(cap[0]) : _escape(cap[0]) : cap[0] + }; + } + }; + + _proto.link = function link(src) { + var cap = this.rules.inline.link.exec(src); + + if (cap) { + var lastParenIndex = findClosingBracket$1(cap[2], '()'); + + if (lastParenIndex > -1) { + var start = cap[0].indexOf('!') === 0 ? 5 : 4; + var linkLen = start + cap[1].length + lastParenIndex; + cap[2] = cap[2].substring(0, lastParenIndex); + cap[0] = cap[0].substring(0, linkLen).trim(); + cap[3] = ''; + } + + var href = cap[2]; + var title = ''; + + if (this.options.pedantic) { + var link = /^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(href); + + if (link) { + href = link[1]; + title = link[3]; } else { - item.align[i] = null; + title = ''; + } + } else { + title = cap[3] ? cap[3].slice(1, -1) : ''; + } + + href = href.trim().replace(/^<([\s\S]*)>$/, '$1'); + var token = outputLink(cap, { + href: href ? href.replace(this.rules.inline._escapes, '$1') : href, + title: title ? title.replace(this.rules.inline._escapes, '$1') : title + }, cap[0]); + return token; + } + }; + + _proto.reflink = function reflink(src, links) { + var cap; + + if ((cap = this.rules.inline.reflink.exec(src)) || (cap = this.rules.inline.nolink.exec(src))) { + var link = (cap[2] || cap[1]).replace(/\s+/g, ' '); + link = links[link.toLowerCase()]; + + if (!link || !link.href) { + var text = cap[0].charAt(0); + return { + type: 'text', + raw: text, + text: text + }; + } + + var token = outputLink(cap, link, cap[0]); + return token; + } + }; + + _proto.strong = function strong(src, maskedSrc, prevChar) { + if (prevChar === void 0) { + prevChar = ''; + } + + var match = this.rules.inline.strong.start.exec(src); + + if (match && (!match[1] || match[1] && (prevChar === '' || this.rules.inline.punctuation.exec(prevChar)))) { + maskedSrc = maskedSrc.slice(-1 * src.length); + var endReg = match[0] === '**' ? this.rules.inline.strong.endAst : this.rules.inline.strong.endUnd; + endReg.lastIndex = 0; + var cap; + + while ((match = endReg.exec(maskedSrc)) != null) { + cap = this.rules.inline.strong.middle.exec(maskedSrc.slice(0, match.index + 3)); + + if (cap) { + return { + type: 'strong', + raw: src.slice(0, cap[0].length), + text: src.slice(2, cap[0].length - 2) + }; + } + } + } + }; + + _proto.em = function em(src, maskedSrc, prevChar) { + if (prevChar === void 0) { + prevChar = ''; + } + + var match = this.rules.inline.em.start.exec(src); + + if (match && (!match[1] || match[1] && (prevChar === '' || this.rules.inline.punctuation.exec(prevChar)))) { + maskedSrc = maskedSrc.slice(-1 * src.length); + var endReg = match[0] === '*' ? this.rules.inline.em.endAst : this.rules.inline.em.endUnd; + endReg.lastIndex = 0; + var cap; + + while ((match = endReg.exec(maskedSrc)) != null) { + cap = this.rules.inline.em.middle.exec(maskedSrc.slice(0, match.index + 2)); + + if (cap) { + return { + type: 'em', + raw: src.slice(0, cap[0].length), + text: src.slice(1, cap[0].length - 1) + }; + } + } + } + }; + + _proto.codespan = function codespan(src) { + var cap = this.rules.inline.code.exec(src); + + if (cap) { + var text = cap[2].replace(/\n/g, ' '); + var hasNonSpaceChars = /[^ ]/.test(text); + var hasSpaceCharsOnBothEnds = text.startsWith(' ') && text.endsWith(' '); + + if (hasNonSpaceChars && hasSpaceCharsOnBothEnds) { + text = text.substring(1, text.length - 1); + } + + text = _escape(text, true); + return { + type: 'codespan', + raw: cap[0], + text: text + }; + } + }; + + _proto.br = function br(src) { + var cap = this.rules.inline.br.exec(src); + + if (cap) { + return { + type: 'br', + raw: cap[0] + }; + } + }; + + _proto.del = function del(src) { + var cap = this.rules.inline.del.exec(src); + + if (cap) { + return { + type: 'del', + raw: cap[0], + text: cap[1] + }; + } + }; + + _proto.autolink = function autolink(src, mangle) { + var cap = this.rules.inline.autolink.exec(src); + + if (cap) { + var text, href; + + if (cap[2] === '@') { + text = _escape(this.options.mangle ? mangle(cap[1]) : cap[1]); + href = 'mailto:' + text; + } else { + text = _escape(cap[1]); + href = text; + } + + return { + type: 'link', + raw: cap[0], + text: text, + href: href, + tokens: [{ + type: 'text', + raw: text, + text: text + }] + }; + } + }; + + _proto.url = function url(src, mangle) { + var cap; + + if (cap = this.rules.inline.url.exec(src)) { + var text, href; + + if (cap[2] === '@') { + text = _escape(this.options.mangle ? mangle(cap[0]) : cap[0]); + href = 'mailto:' + text; + } else { + // do extended autolink path validation + var prevCapZero; + + do { + prevCapZero = cap[0]; + cap[0] = this.rules.inline._backpedal.exec(cap[0])[0]; + } while (prevCapZero !== cap[0]); + + text = _escape(cap[0]); + + if (cap[1] === 'www.') { + href = 'http://' + text; + } else { + href = text; } } - for (i = 0; i < item.cells.length; i++) { - item.cells[i] = splitCells( - item.cells[i].replace(/^ *\| *| *\| *$/g, ''), - item.header.length); + return { + type: 'link', + raw: cap[0], + text: text, + href: href, + tokens: [{ + type: 'text', + raw: text, + text: text + }] + }; + } + }; + + _proto.inlineText = function inlineText(src, inRawBlock, smartypants) { + var cap = this.rules.inline.text.exec(src); + + if (cap) { + var text; + + if (inRawBlock) { + text = this.options.sanitize ? this.options.sanitizer ? this.options.sanitizer(cap[0]) : _escape(cap[0]) : cap[0]; + } else { + text = _escape(this.options.smartypants ? smartypants(cap[0]) : cap[0]); } - this.tokens.push(item); - - continue; + return { + type: 'text', + raw: cap[0], + text: text + }; } - } + }; - // lheading - if (cap = this.rules.lheading.exec(src)) { - src = src.substring(cap[0].length); - this.tokens.push({ - type: 'heading', - depth: cap[2] === '=' ? 1 : 2, - text: cap[1] - }); - continue; - } + return Tokenizer; + }(); - // top-level paragraph - if (top && (cap = this.rules.paragraph.exec(src))) { - src = src.substring(cap[0].length); - this.tokens.push({ - type: 'paragraph', - text: cap[1].charAt(cap[1].length - 1) === '\n' - ? cap[1].slice(0, -1) - : cap[1] - }); - continue; - } + var noopTest$1 = helpers.noopTest, + edit$1 = helpers.edit, + merge$1 = helpers.merge; + /** + * Block-Level Grammar + */ - // text - if (cap = this.rules.text.exec(src)) { - // Top-level should never reach here. - src = src.substring(cap[0].length); - this.tokens.push({ - type: 'text', - text: cap[0] - }); - continue; - } + var block = { + newline: /^\n+/, + code: /^( {4}[^\n]+\n*)+/, + fences: /^ {0,3}(`{3,}(?=[^`\n]*\n)|~{3,})([^\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?:\n+|$)|$)/, + hr: /^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/, + heading: /^ {0,3}(#{1,6}) +([^\n]*?)(?: +#+)? *(?:\n+|$)/, + blockquote: /^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/, + list: /^( {0,3})(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, + html: '^ {0,3}(?:' // optional indentation + + '<(script|pre|style)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)' // (1) + + '|comment[^\\n]*(\\n+|$)' // (2) + + '|<\\?[\\s\\S]*?(?:\\?>\\n*|$)' // (3) + + '|\\n*|$)' // (4) + + '|\\n*|$)' // (5) + + '|)[\\s\\S]*?(?:\\n{2,}|$)' // (6) + + '|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$)' // (7) open tag + + '|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$)' // (7) closing tag + + ')', + def: /^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/, + nptable: noopTest$1, + table: noopTest$1, + lheading: /^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/, + // regex template, placeholders will be replaced according to different paragraph + // interruption rules of commonmark and the original markdown spec: + _paragraph: /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html)[^\n]+)*)/, + text: /^[^\n]+/ + }; + block._label = /(?!\s*\])(?:\\[\[\]]|[^\[\]])+/; + block._title = /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/; + block.def = edit$1(block.def).replace('label', block._label).replace('title', block._title).getRegex(); + block.bullet = /(?:[*+-]|\d{1,9}[.)])/; + block.item = /^( *)(bull) ?[^\n]*(?:\n(?!\1bull ?)[^\n]*)*/; + block.item = edit$1(block.item, 'gm').replace(/bull/g, block.bullet).getRegex(); + block.list = edit$1(block.list).replace(/bull/g, block.bullet).replace('hr', '\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))').replace('def', '\\n+(?=' + block.def.source + ')').getRegex(); + block._tag = 'address|article|aside|base|basefont|blockquote|body|caption' + '|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption' + '|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe' + '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option' + '|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr' + '|track|ul'; + block._comment = /|$)/; + block.html = edit$1(block.html, 'i').replace('comment', block._comment).replace('tag', block._tag).replace('attribute', / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(); + block.paragraph = edit$1(block._paragraph).replace('hr', block.hr).replace('heading', ' {0,3}#{1,6} ').replace('|lheading', '') // setex headings don't interrupt commonmark paragraphs + .replace('blockquote', ' {0,3}>').replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n').replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|!--)').replace('tag', block._tag) // pars can be interrupted by type (6) html blocks + .getRegex(); + block.blockquote = edit$1(block.blockquote).replace('paragraph', block.paragraph).getRegex(); + /** + * Normal Block Grammar + */ - if (src) { - throw new Error('Infinite loop on byte: ' + src.charCodeAt(0)); - } - } + block.normal = merge$1({}, block); + /** + * GFM Block Grammar + */ - return this.tokens; -}; + block.gfm = merge$1({}, block.normal, { + nptable: '^ *([^|\\n ].*\\|.*)\\n' // Header + + ' *([-:]+ *\\|[-| :]*)' // Align + + '(?:\\n((?:(?!\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)', + // Cells + table: '^ *\\|(.+)\\n' // Header + + ' *\\|?( *[-:]+[-| :]*)' // Align + + '(?:\\n *((?:(?!\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)' // Cells -/** - * Inline-Level Grammar - */ + }); + block.gfm.nptable = edit$1(block.gfm.nptable).replace('hr', block.hr).replace('heading', ' {0,3}#{1,6} ').replace('blockquote', ' {0,3}>').replace('code', ' {4}[^\\n]').replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n').replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|!--)').replace('tag', block._tag) // tables can be interrupted by type (6) html blocks + .getRegex(); + block.gfm.table = edit$1(block.gfm.table).replace('hr', block.hr).replace('heading', ' {0,3}#{1,6} ').replace('blockquote', ' {0,3}>').replace('code', ' {4}[^\\n]').replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n').replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|!--)').replace('tag', block._tag) // tables can be interrupted by type (6) html blocks + .getRegex(); + /** + * Pedantic grammar (original John Gruber's loose markdown specification) + */ -var inline = { - escape: /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/, - autolink: /^<(scheme:[^\s\x00-\x1f<>]*|email)>/, - url: noop, - tag: '^comment' - + '|^' // self-closing tag + block.pedantic = merge$1({}, block.normal, { + html: edit$1('^ *(?:comment *(?:\\n|\\s*$)' + '|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)' // closed tag + + '|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))').replace('comment', block._comment).replace(/tag/g, '(?!(?:' + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub' + '|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)' + '\\b)\\w+(?!:|[^\\w\\s@]*@)\\b').getRegex(), + def: /^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, + heading: /^ *(#{1,6}) *([^\n]+?) *(?:#+ *)?(?:\n+|$)/, + fences: noopTest$1, + // fences not supported + paragraph: edit$1(block.normal._paragraph).replace('hr', block.hr).replace('heading', ' *#{1,6} *[^\n]').replace('lheading', block.lheading).replace('blockquote', ' {0,3}>').replace('|fences', '').replace('|list', '').replace('|html', '').getRegex() + }); + /** + * Inline-Level Grammar + */ + + var inline = { + escape: /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/, + autolink: /^<(scheme:[^\s\x00-\x1f<>]*|email)>/, + url: noopTest$1, + tag: '^comment' + '|^' // self-closing tag + '|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>' // open tag + '|^<\\?[\\s\\S]*?\\?>' // processing instruction, e.g. + '|^' // declaration, e.g. - + '|^', // CDATA section - link: /^!?\[(label)\]\(href(?:\s+(title))?\s*\)/, - reflink: /^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/, - nolink: /^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/, - strong: /^__([^\s_])__(?!_)|^\*\*([^\s*])\*\*(?!\*)|^__([^\s][\s\S]*?[^\s])__(?!_)|^\*\*([^\s][\s\S]*?[^\s])\*\*(?!\*)/, - em: /^_([^\s_])_(?!_)|^\*([^\s*"<\[])\*(?!\*)|^_([^\s][\s\S]*?[^\s_])_(?!_|[^\spunctuation])|^_([^\s_][\s\S]*?[^\s])_(?!_|[^\spunctuation])|^\*([^\s"<\[][\s\S]*?[^\s*])\*(?!\*)|^\*([^\s*"<\[][\s\S]*?[^\s])\*(?!\*)/, - code: /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/, - br: /^( {2,}|\\)\n(?!\s*$)/, - del: noop, - text: /^(`+|[^`])(?:[\s\S]*?(?:(?=[\\', + // CDATA section + link: /^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/, + reflink: /^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/, + nolink: /^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/, + reflinkSearch: 'reflink|nolink(?!\\()', + strong: { + start: /^(?:(\*\*(?=[*punctuation]))|\*\*)(?![\s])|__/, + // (1) returns if starts w/ punctuation + middle: /^\*\*(?:(?:(?!overlapSkip)(?:[^*]|\\\*)|overlapSkip)|\*(?:(?!overlapSkip)(?:[^*]|\\\*)|overlapSkip)*?\*)+?\*\*$|^__(?![\s])((?:(?:(?!overlapSkip)(?:[^_]|\\_)|overlapSkip)|_(?:(?!overlapSkip)(?:[^_]|\\_)|overlapSkip)*?_)+?)__$/, + endAst: /[^punctuation\s]\*\*(?!\*)|[punctuation]\*\*(?!\*)(?:(?=[punctuation\s]|$))/, + // last char can't be punct, or final * must also be followed by punct (or endline) + endUnd: /[^\s]__(?!_)(?:(?=[punctuation\s])|$)/ // last char can't be a space, and final _ must preceed punct or \s (or endline) -// list of punctuation marks from common mark spec -// without ` and ] to workaround Rule 17 (inline code blocks/links) -inline._punctuation = '!"#$%&\'()*+,\\-./:;<=>?@\\[^_{|}~'; -inline.em = edit(inline.em).replace(/punctuation/g, inline._punctuation).getRegex(); + }, + em: { + start: /^(?:(\*(?=[punctuation]))|\*)(?![*\s])|_/, + // (1) returns if starts w/ punctuation + middle: /^\*(?:(?:(?!overlapSkip)(?:[^*]|\\\*)|overlapSkip)|\*(?:(?!overlapSkip)(?:[^*]|\\\*)|overlapSkip)*?\*)+?\*$|^_(?![_\s])(?:(?:(?!overlapSkip)(?:[^_]|\\_)|overlapSkip)|_(?:(?!overlapSkip)(?:[^_]|\\_)|overlapSkip)*?_)+?_$/, + endAst: /[^punctuation\s]\*(?!\*)|[punctuation]\*(?!\*)(?:(?=[punctuation\s]|$))/, + // last char can't be punct, or final * must also be followed by punct (or endline) + endUnd: /[^\s]_(?!_)(?:(?=[punctuation\s])|$)/ // last char can't be a space, and final _ must preceed punct or \s (or endline) -inline._escapes = /\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g; + }, + code: /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/, + br: /^( {2,}|\\)\n(?!\s*$)/, + del: noopTest$1, + text: /^(`+|[^`])(?:[\s\S]*?(?:(?=[\\?@\\[\\]`^{|}~'; + inline.punctuation = edit$1(inline.punctuation).replace(/punctuation/g, inline._punctuation).getRegex(); // sequences em should skip over [title](link), `code`, -inline._attribute = /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/; + inline._blockSkip = '\\[[^\\]]*?\\]\\([^\\)]*?\\)|`[^`]*?`|<[^>]*?>'; + inline._overlapSkip = '__[^_]*?__|\\*\\*\\[^\\*\\]*?\\*\\*'; + inline._comment = edit$1(block._comment).replace('(?:-->|$)', '-->').getRegex(); + inline.em.start = edit$1(inline.em.start).replace(/punctuation/g, inline._punctuation).getRegex(); + inline.em.middle = edit$1(inline.em.middle).replace(/punctuation/g, inline._punctuation).replace(/overlapSkip/g, inline._overlapSkip).getRegex(); + inline.em.endAst = edit$1(inline.em.endAst, 'g').replace(/punctuation/g, inline._punctuation).getRegex(); + inline.em.endUnd = edit$1(inline.em.endUnd, 'g').replace(/punctuation/g, inline._punctuation).getRegex(); + inline.strong.start = edit$1(inline.strong.start).replace(/punctuation/g, inline._punctuation).getRegex(); + inline.strong.middle = edit$1(inline.strong.middle).replace(/punctuation/g, inline._punctuation).replace(/blockSkip/g, inline._blockSkip).getRegex(); + inline.strong.endAst = edit$1(inline.strong.endAst, 'g').replace(/punctuation/g, inline._punctuation).getRegex(); + inline.strong.endUnd = edit$1(inline.strong.endUnd, 'g').replace(/punctuation/g, inline._punctuation).getRegex(); + inline.blockSkip = edit$1(inline._blockSkip, 'g').getRegex(); + inline.overlapSkip = edit$1(inline._overlapSkip, 'g').getRegex(); + inline._escapes = /\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g; + inline._scheme = /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/; + inline._email = /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/; + inline.autolink = edit$1(inline.autolink).replace('scheme', inline._scheme).replace('email', inline._email).getRegex(); + inline._attribute = /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/; + inline.tag = edit$1(inline.tag).replace('comment', inline._comment).replace('attribute', inline._attribute).getRegex(); + inline._label = /(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/; + inline._href = /<(?:\\[<>]?|[^\s<>\\])*>|[^\s\x00-\x1f]*/; + inline._title = /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/; + inline.link = edit$1(inline.link).replace('label', inline._label).replace('href', inline._href).replace('title', inline._title).getRegex(); + inline.reflink = edit$1(inline.reflink).replace('label', inline._label).getRegex(); + inline.reflinkSearch = edit$1(inline.reflinkSearch, 'g').replace('reflink', inline.reflink).replace('nolink', inline.nolink).getRegex(); + /** + * Normal Inline Grammar + */ -inline.tag = edit(inline.tag) - .replace('comment', block._comment) - .replace('attribute', inline._attribute) - .getRegex(); + inline.normal = merge$1({}, inline); + /** + * Pedantic Inline Grammar + */ -inline._label = /(?:\[[^\[\]]*\]|\\[\[\]]?|`[^`]*`|`(?!`)|[^\[\]\\`])*?/; -inline._href = /\s*(<(?:\\[<>]?|[^\s<>\\])*>|[^\s\x00-\x1f]*)/; -inline._title = /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/; + inline.pedantic = merge$1({}, inline.normal, { + strong: { + start: /^__|\*\*/, + middle: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, + endAst: /\*\*(?!\*)/g, + endUnd: /__(?!_)/g + }, + em: { + start: /^_|\*/, + middle: /^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/, + endAst: /\*(?!\*)/g, + endUnd: /_(?!_)/g + }, + link: edit$1(/^!?\[(label)\]\((.*?)\)/).replace('label', inline._label).getRegex(), + reflink: edit$1(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace('label', inline._label).getRegex() + }); + /** + * GFM Inline Grammar + */ -inline.link = edit(inline.link) - .replace('label', inline._label) - .replace('href', inline._href) - .replace('title', inline._title) - .getRegex(); + inline.gfm = merge$1({}, inline.normal, { + escape: edit$1(inline.escape).replace('])', '~|])').getRegex(), + _extended_email: /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/, + url: /^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/, + _backpedal: /(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/, + del: /^~+(?=\S)([\s\S]*?\S)~+/, + text: /^(`+|[^`])(?:[\s\S]*?(?:(?=[\\ 0.5) { + ch = 'x' + ch.toString(16); + } + + out += '&#' + ch + ';'; } + + return out; } -} + /** + * Block Lexer + */ -/** - * Expose Inline Rules - */ -InlineLexer.rules = inline; + var Lexer_1 = /*#__PURE__*/function () { + function Lexer(options) { + this.tokens = []; + this.tokens.links = Object.create(null); + this.options = options || defaults$2; + this.options.tokenizer = this.options.tokenizer || new Tokenizer_1(); + this.tokenizer = this.options.tokenizer; + this.tokenizer.options = this.options; + var rules = { + block: block$1.normal, + inline: inline$1.normal + }; -/** - * Static Lexing/Compiling Method - */ - -InlineLexer.output = function(src, links, options) { - var inline = new InlineLexer(links, options); - return inline.output(src); -}; - -/** - * Lexing/Compiling - */ - -InlineLexer.prototype.output = function(src) { - var out = '', - link, - text, - href, - title, - cap, - prevCapZero; - - while (src) { - // escape - if (cap = this.rules.escape.exec(src)) { - src = src.substring(cap[0].length); - out += escape(cap[1]); - continue; - } - - // tag - if (cap = this.rules.tag.exec(src)) { - if (!this.inLink && /^/i.test(cap[0])) { - this.inLink = false; - } - if (!this.inRawBlock && /^<(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { - this.inRawBlock = true; - } else if (this.inRawBlock && /^<\/(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { - this.inRawBlock = false; - } - - src = src.substring(cap[0].length); - out += this.options.sanitize - ? this.options.sanitizer - ? this.options.sanitizer(cap[0]) - : escape(cap[0]) - : cap[0]; - continue; - } - - // link - if (cap = this.rules.link.exec(src)) { - var lastParenIndex = findClosingBracket(cap[2], '()'); - if (lastParenIndex > -1) { - var linkLen = cap[0].length - (cap[2].length - lastParenIndex) - (cap[3] || '').length; - cap[2] = cap[2].substring(0, lastParenIndex); - cap[0] = cap[0].substring(0, linkLen).trim(); - cap[3] = ''; - } - src = src.substring(cap[0].length); - this.inLink = true; - href = cap[2]; if (this.options.pedantic) { - link = /^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(href); + rules.block = block$1.pedantic; + rules.inline = inline$1.pedantic; + } else if (this.options.gfm) { + rules.block = block$1.gfm; - if (link) { - href = link[1]; - title = link[3]; + if (this.options.breaks) { + rules.inline = inline$1.breaks; } else { - title = ''; - } - } else { - title = cap[3] ? cap[3].slice(1, -1) : ''; - } - href = href.trim().replace(/^<([\s\S]*)>$/, '$1'); - out += this.outputLink(cap, { - href: InlineLexer.escapes(href), - title: InlineLexer.escapes(title) - }); - this.inLink = false; - continue; - } - - // reflink, nolink - if ((cap = this.rules.reflink.exec(src)) - || (cap = this.rules.nolink.exec(src))) { - src = src.substring(cap[0].length); - link = (cap[2] || cap[1]).replace(/\s+/g, ' '); - link = this.links[link.toLowerCase()]; - if (!link || !link.href) { - out += cap[0].charAt(0); - src = cap[0].substring(1) + src; - continue; - } - this.inLink = true; - out += this.outputLink(cap, link); - this.inLink = false; - continue; - } - - // strong - if (cap = this.rules.strong.exec(src)) { - src = src.substring(cap[0].length); - out += this.renderer.strong(this.output(cap[4] || cap[3] || cap[2] || cap[1])); - continue; - } - - // em - if (cap = this.rules.em.exec(src)) { - src = src.substring(cap[0].length); - out += this.renderer.em(this.output(cap[6] || cap[5] || cap[4] || cap[3] || cap[2] || cap[1])); - continue; - } - - // code - if (cap = this.rules.code.exec(src)) { - src = src.substring(cap[0].length); - out += this.renderer.codespan(escape(cap[2].trim(), true)); - continue; - } - - // br - if (cap = this.rules.br.exec(src)) { - src = src.substring(cap[0].length); - out += this.renderer.br(); - continue; - } - - // del (gfm) - if (cap = this.rules.del.exec(src)) { - src = src.substring(cap[0].length); - out += this.renderer.del(this.output(cap[1])); - continue; - } - - // autolink - if (cap = this.rules.autolink.exec(src)) { - src = src.substring(cap[0].length); - if (cap[2] === '@') { - text = escape(this.mangle(cap[1])); - href = 'mailto:' + text; - } else { - text = escape(cap[1]); - href = text; - } - out += this.renderer.link(href, null, text); - continue; - } - - // url (gfm) - if (!this.inLink && (cap = this.rules.url.exec(src))) { - if (cap[2] === '@') { - text = escape(cap[0]); - href = 'mailto:' + text; - } else { - // do extended autolink path validation - do { - prevCapZero = cap[0]; - cap[0] = this.rules._backpedal.exec(cap[0])[0]; - } while (prevCapZero !== cap[0]); - text = escape(cap[0]); - if (cap[1] === 'www.') { - href = 'http://' + text; - } else { - href = text; + rules.inline = inline$1.gfm; } } - src = src.substring(cap[0].length); - out += this.renderer.link(href, null, text); - continue; - } - // text - if (cap = this.rules.text.exec(src)) { - src = src.substring(cap[0].length); - if (this.inRawBlock) { - out += this.renderer.text(cap[0]); - } else { - out += this.renderer.text(escape(this.smartypants(cap[0]))); + this.tokenizer.rules = rules; + } + /** + * Expose Rules + */ + + + /** + * Static Lex Method + */ + Lexer.lex = function lex(src, options) { + var lexer = new Lexer(options); + return lexer.lex(src); + } + /** + * Preprocessing + */ + ; + + var _proto = Lexer.prototype; + + _proto.lex = function lex(src) { + src = src.replace(/\r\n|\r/g, '\n').replace(/\t/g, ' '); + this.blockTokens(src, this.tokens, true); + this.inline(this.tokens); + return this.tokens; + } + /** + * Lexing + */ + ; + + _proto.blockTokens = function blockTokens(src, tokens, top) { + if (tokens === void 0) { + tokens = []; } - continue; + + if (top === void 0) { + top = true; + } + + src = src.replace(/^ +$/gm, ''); + var token, i, l, lastToken; + + while (src) { + // newline + if (token = this.tokenizer.space(src)) { + src = src.substring(token.raw.length); + + if (token.type) { + tokens.push(token); + } + + continue; + } // code + + + if (token = this.tokenizer.code(src, tokens)) { + src = src.substring(token.raw.length); + + if (token.type) { + tokens.push(token); + } else { + lastToken = tokens[tokens.length - 1]; + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + } + + continue; + } // fences + + + if (token = this.tokenizer.fences(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // heading + + + if (token = this.tokenizer.heading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // table no leading pipe (gfm) + + + if (token = this.tokenizer.nptable(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // hr + + + if (token = this.tokenizer.hr(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // blockquote + + + if (token = this.tokenizer.blockquote(src)) { + src = src.substring(token.raw.length); + token.tokens = this.blockTokens(token.text, [], top); + tokens.push(token); + continue; + } // list + + + if (token = this.tokenizer.list(src)) { + src = src.substring(token.raw.length); + l = token.items.length; + + for (i = 0; i < l; i++) { + token.items[i].tokens = this.blockTokens(token.items[i].text, [], false); + } + + tokens.push(token); + continue; + } // html + + + if (token = this.tokenizer.html(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // def + + + if (top && (token = this.tokenizer.def(src))) { + src = src.substring(token.raw.length); + + if (!this.tokens.links[token.tag]) { + this.tokens.links[token.tag] = { + href: token.href, + title: token.title + }; + } + + continue; + } // table (gfm) + + + if (token = this.tokenizer.table(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // lheading + + + if (token = this.tokenizer.lheading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // top-level paragraph + + + if (top && (token = this.tokenizer.paragraph(src))) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // text + + + if (token = this.tokenizer.text(src, tokens)) { + src = src.substring(token.raw.length); + + if (token.type) { + tokens.push(token); + } else { + lastToken = tokens[tokens.length - 1]; + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + } + + continue; + } + + if (src) { + var errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + + if (this.options.silent) { + console.error(errMsg); + break; + } else { + throw new Error(errMsg); + } + } + } + + return tokens; + }; + + _proto.inline = function inline(tokens) { + var i, j, k, l2, row, token; + var l = tokens.length; + + for (i = 0; i < l; i++) { + token = tokens[i]; + + switch (token.type) { + case 'paragraph': + case 'text': + case 'heading': + { + token.tokens = []; + this.inlineTokens(token.text, token.tokens); + break; + } + + case 'table': + { + token.tokens = { + header: [], + cells: [] + }; // header + + l2 = token.header.length; + + for (j = 0; j < l2; j++) { + token.tokens.header[j] = []; + this.inlineTokens(token.header[j], token.tokens.header[j]); + } // cells + + + l2 = token.cells.length; + + for (j = 0; j < l2; j++) { + row = token.cells[j]; + token.tokens.cells[j] = []; + + for (k = 0; k < row.length; k++) { + token.tokens.cells[j][k] = []; + this.inlineTokens(row[k], token.tokens.cells[j][k]); + } + } + + break; + } + + case 'blockquote': + { + this.inline(token.tokens); + break; + } + + case 'list': + { + l2 = token.items.length; + + for (j = 0; j < l2; j++) { + this.inline(token.items[j].tokens); + } + + break; + } + } + } + + return tokens; + } + /** + * Lexing/Compiling + */ + ; + + _proto.inlineTokens = function inlineTokens(src, tokens, inLink, inRawBlock, prevChar) { + if (tokens === void 0) { + tokens = []; + } + + if (inLink === void 0) { + inLink = false; + } + + if (inRawBlock === void 0) { + inRawBlock = false; + } + + if (prevChar === void 0) { + prevChar = ''; + } + + var token; // String with links masked to avoid interference with em and strong + + var maskedSrc = src; + var match; // Mask out reflinks + + if (this.tokens.links) { + var links = Object.keys(this.tokens.links); + + if (links.length > 0) { + while ((match = this.tokenizer.rules.inline.reflinkSearch.exec(maskedSrc)) != null) { + if (links.includes(match[0].slice(match[0].lastIndexOf('[') + 1, -1))) { + maskedSrc = maskedSrc.slice(0, match.index) + '[' + 'a'.repeat(match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex); + } + } + } + } // Mask out other blocks + + + while ((match = this.tokenizer.rules.inline.blockSkip.exec(maskedSrc)) != null) { + maskedSrc = maskedSrc.slice(0, match.index) + '[' + 'a'.repeat(match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.blockSkip.lastIndex); + } + + while (src) { + // escape + if (token = this.tokenizer.escape(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // tag + + + if (token = this.tokenizer.tag(src, inLink, inRawBlock)) { + src = src.substring(token.raw.length); + inLink = token.inLink; + inRawBlock = token.inRawBlock; + tokens.push(token); + continue; + } // link + + + if (token = this.tokenizer.link(src)) { + src = src.substring(token.raw.length); + + if (token.type === 'link') { + token.tokens = this.inlineTokens(token.text, [], true, inRawBlock); + } + + tokens.push(token); + continue; + } // reflink, nolink + + + if (token = this.tokenizer.reflink(src, this.tokens.links)) { + src = src.substring(token.raw.length); + + if (token.type === 'link') { + token.tokens = this.inlineTokens(token.text, [], true, inRawBlock); + } + + tokens.push(token); + continue; + } // strong + + + if (token = this.tokenizer.strong(src, maskedSrc, prevChar)) { + src = src.substring(token.raw.length); + token.tokens = this.inlineTokens(token.text, [], inLink, inRawBlock); + tokens.push(token); + continue; + } // em + + + if (token = this.tokenizer.em(src, maskedSrc, prevChar)) { + src = src.substring(token.raw.length); + token.tokens = this.inlineTokens(token.text, [], inLink, inRawBlock); + tokens.push(token); + continue; + } // code + + + if (token = this.tokenizer.codespan(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // br + + + if (token = this.tokenizer.br(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // del (gfm) + + + if (token = this.tokenizer.del(src)) { + src = src.substring(token.raw.length); + token.tokens = this.inlineTokens(token.text, [], inLink, inRawBlock); + tokens.push(token); + continue; + } // autolink + + + if (token = this.tokenizer.autolink(src, mangle)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // url (gfm) + + + if (!inLink && (token = this.tokenizer.url(src, mangle))) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // text + + + if (token = this.tokenizer.inlineText(src, inRawBlock, smartypants)) { + src = src.substring(token.raw.length); + prevChar = token.raw.slice(-1); + tokens.push(token); + continue; + } + + if (src) { + var errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + + if (this.options.silent) { + console.error(errMsg); + break; + } else { + throw new Error(errMsg); + } + } + } + + return tokens; + }; + + _createClass(Lexer, null, [{ + key: "rules", + get: function get() { + return { + block: block$1, + inline: inline$1 + }; + } + }]); + + return Lexer; + }(); + + var defaults$3 = defaults.defaults; + var cleanUrl$1 = helpers.cleanUrl, + escape$1 = helpers.escape; + /** + * Renderer + */ + + var Renderer_1 = /*#__PURE__*/function () { + function Renderer(options) { + this.options = options || defaults$3; } - if (src) { - throw new Error('Infinite loop on byte: ' + src.charCodeAt(0)); - } - } + var _proto = Renderer.prototype; - return out; -}; + _proto.code = function code(_code, infostring, escaped) { + var lang = (infostring || '').match(/\S*/)[0]; -InlineLexer.escapes = function(text) { - return text ? text.replace(InlineLexer.rules._escapes, '$1') : text; -}; + if (this.options.highlight) { + var out = this.options.highlight(_code, lang); -/** - * Compile Link - */ + if (out != null && out !== _code) { + escaped = true; + _code = out; + } + } -InlineLexer.prototype.outputLink = function(cap, link) { - var href = link.href, - title = link.title ? escape(link.title) : null; + if (!lang) { + return '
' + (escaped ? _code : escape$1(_code, true)) + '
\n'; + } - return cap[0].charAt(0) !== '!' - ? this.renderer.link(href, title, this.output(cap[1])) - : this.renderer.image(href, title, escape(cap[1])); -}; + return '
' + (escaped ? _code : escape$1(_code, true)) + '
\n'; + }; -/** - * Smartypants Transformations - */ + _proto.blockquote = function blockquote(quote) { + return '
\n' + quote + '
\n'; + }; -InlineLexer.prototype.smartypants = function(text) { - if (!this.options.smartypants) return text; - return text - // em-dashes - .replace(/---/g, '\u2014') - // en-dashes - .replace(/--/g, '\u2013') - // opening singles - .replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018') - // closing singles & apostrophes - .replace(/'/g, '\u2019') - // opening doubles - .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c') - // closing doubles - .replace(/"/g, '\u201d') - // ellipses - .replace(/\.{3}/g, '\u2026'); -}; + _proto.html = function html(_html) { + return _html; + }; -/** - * Mangle Links - */ + _proto.heading = function heading(text, level, raw, slugger) { + if (this.options.headerIds) { + return '' + text + '\n'; + } // ignore IDs -InlineLexer.prototype.mangle = function(text) { - if (!this.options.mangle) return text; - var out = '', - l = text.length, - i = 0, - ch; - for (; i < l; i++) { - ch = text.charCodeAt(i); - if (Math.random() > 0.5) { - ch = 'x' + ch.toString(16); - } - out += '&#' + ch + ';'; - } + return '' + text + '\n'; + }; - return out; -}; + _proto.hr = function hr() { + return this.options.xhtml ? '
\n' : '
\n'; + }; -/** - * Renderer - */ + _proto.list = function list(body, ordered, start) { + var type = ordered ? 'ol' : 'ul', + startatt = ordered && start !== 1 ? ' start="' + start + '"' : ''; + return '<' + type + startatt + '>\n' + body + '\n'; + }; -function Renderer(options) { - this.options = options || marked.defaults; -} + _proto.listitem = function listitem(text) { + return '
  • ' + text + '
  • \n'; + }; -Renderer.prototype.code = function(code, infostring, escaped) { - var lang = (infostring || '').match(/\S*/)[0]; - if (this.options.highlight) { - var out = this.options.highlight(code, lang); - if (out != null && out !== code) { - escaped = true; - code = out; - } - } + _proto.checkbox = function checkbox(checked) { + return ' '; + }; - if (!lang) { - return '
    '
    -      + (escaped ? code : escape(code, true))
    -      + '
    '; - } + _proto.paragraph = function paragraph(text) { + return '

    ' + text + '

    \n'; + }; - return '
    '
    -    + (escaped ? code : escape(code, true))
    -    + '
    \n'; -}; + _proto.table = function table(header, body) { + if (body) body = '' + body + ''; + return '\n' + '\n' + header + '\n' + body + '
    \n'; + }; -Renderer.prototype.blockquote = function(quote) { - return '
    \n' + quote + '
    \n'; -}; + _proto.tablerow = function tablerow(content) { + return '\n' + content + '\n'; + }; -Renderer.prototype.html = function(html) { - return html; -}; + _proto.tablecell = function tablecell(content, flags) { + var type = flags.header ? 'th' : 'td'; + var tag = flags.align ? '<' + type + ' align="' + flags.align + '">' : '<' + type + '>'; + return tag + content + '\n'; + } // span level renderer + ; -Renderer.prototype.heading = function(text, level, raw, slugger) { - if (this.options.headerIds) { - return '' - + text - + '\n'; - } - // ignore IDs - return '' + text + '\n'; -}; + _proto.strong = function strong(text) { + return '' + text + ''; + }; -Renderer.prototype.hr = function() { - return this.options.xhtml ? '
    \n' : '
    \n'; -}; + _proto.em = function em(text) { + return '' + text + ''; + }; -Renderer.prototype.list = function(body, ordered, start) { - var type = ordered ? 'ol' : 'ul', - startatt = (ordered && start !== 1) ? (' start="' + start + '"') : ''; - return '<' + type + startatt + '>\n' + body + '\n'; -}; + _proto.codespan = function codespan(text) { + return '' + text + ''; + }; -Renderer.prototype.listitem = function(text) { - return '
  • ' + text + '
  • \n'; -}; + _proto.br = function br() { + return this.options.xhtml ? '
    ' : '
    '; + }; -Renderer.prototype.checkbox = function(checked) { - return ' '; -}; + _proto.del = function del(text) { + return '' + text + ''; + }; -Renderer.prototype.paragraph = function(text) { - return '

    ' + text + '

    \n'; -}; + _proto.link = function link(href, title, text) { + href = cleanUrl$1(this.options.sanitize, this.options.baseUrl, href); -Renderer.prototype.table = function(header, body) { - if (body) body = '' + body + ''; + if (href === null) { + return text; + } - return '\n' - + '\n' - + header - + '\n' - + body - + '
    \n'; -}; + var out = '
    \n' + content + '\n'; -}; + if (title) { + out += ' title="' + title + '"'; + } -Renderer.prototype.tablecell = function(content, flags) { - var type = flags.header ? 'th' : 'td'; - var tag = flags.align - ? '<' + type + ' align="' + flags.align + '">' - : '<' + type + '>'; - return tag + content + '\n'; -}; + out += '>' + text + ''; + return out; + }; -// span level renderer -Renderer.prototype.strong = function(text) { - return '' + text + ''; -}; + _proto.image = function image(href, title, text) { + href = cleanUrl$1(this.options.sanitize, this.options.baseUrl, href); -Renderer.prototype.em = function(text) { - return '' + text + ''; -}; + if (href === null) { + return text; + } -Renderer.prototype.codespan = function(text) { - return '' + text + ''; -}; + var out = '' + text + '' : '
    '; -}; + if (title) { + out += ' title="' + title + '"'; + } -Renderer.prototype.del = function(text) { - return '' + text + ''; -}; + out += this.options.xhtml ? '/>' : '>'; + return out; + }; -Renderer.prototype.link = function(href, title, text) { - href = cleanUrl(this.options.sanitize, this.options.baseUrl, href); - if (href === null) { - return text; - } - var out = ''; - return out; -}; + _proto.text = function text(_text) { + return _text; + }; -Renderer.prototype.image = function(href, title, text) { - href = cleanUrl(this.options.sanitize, this.options.baseUrl, href); - if (href === null) { - return text; - } + return Renderer; + }(); - var out = '' + text + '' : '>'; - return out; -}; + /** + * TextRenderer + * returns only the textual part of the token + */ + var TextRenderer_1 = /*#__PURE__*/function () { + function TextRenderer() {} -Renderer.prototype.text = function(text) { - return text; -}; + var _proto = TextRenderer.prototype; -/** - * TextRenderer - * returns only the textual part of the token - */ + // no need for block level renderers + _proto.strong = function strong(text) { + return text; + }; -function TextRenderer() {} + _proto.em = function em(text) { + return text; + }; -// no need for block level renderers + _proto.codespan = function codespan(text) { + return text; + }; -TextRenderer.prototype.strong = -TextRenderer.prototype.em = -TextRenderer.prototype.codespan = -TextRenderer.prototype.del = -TextRenderer.prototype.text = function (text) { - return text; -}; + _proto.del = function del(text) { + return text; + }; -TextRenderer.prototype.link = -TextRenderer.prototype.image = function(href, title, text) { - return '' + text; -}; + _proto.html = function html(text) { + return text; + }; -TextRenderer.prototype.br = function() { - return ''; -}; + _proto.text = function text(_text) { + return _text; + }; -/** - * Parsing & Compiling - */ + _proto.link = function link(href, title, text) { + return '' + text; + }; -function Parser(options) { - this.tokens = []; - this.token = null; - this.options = options || marked.defaults; - this.options.renderer = this.options.renderer || new Renderer(); - this.renderer = this.options.renderer; - this.renderer.options = this.options; - this.slugger = new Slugger(); -} + _proto.image = function image(href, title, text) { + return '' + text; + }; -/** - * Static Parse Method - */ - -Parser.parse = function(src, options) { - var parser = new Parser(options); - return parser.parse(src); -}; - -/** - * Parse Loop - */ - -Parser.prototype.parse = function(src) { - this.inline = new InlineLexer(src.links, this.options); - // use an InlineLexer with a TextRenderer to extract pure text - this.inlineText = new InlineLexer( - src.links, - merge({}, this.options, {renderer: new TextRenderer()}) - ); - this.tokens = src.reverse(); - - var out = ''; - while (this.next()) { - out += this.tok(); - } - - return out; -}; - -/** - * Next Token - */ - -Parser.prototype.next = function() { - return this.token = this.tokens.pop(); -}; - -/** - * Preview Next Token - */ - -Parser.prototype.peek = function() { - return this.tokens[this.tokens.length - 1] || 0; -}; - -/** - * Parse Text Tokens - */ - -Parser.prototype.parseText = function() { - var body = this.token.text; - - while (this.peek().type === 'text') { - body += '\n' + this.next().text; - } - - return this.inline.output(body); -}; - -/** - * Parse Current Token - */ - -Parser.prototype.tok = function() { - switch (this.token.type) { - case 'space': { + _proto.br = function br() { return ''; + }; + + return TextRenderer; + }(); + + /** + * Slugger generates header id + */ + var Slugger_1 = /*#__PURE__*/function () { + function Slugger() { + this.seen = {}; } - case 'hr': { - return this.renderer.hr(); + + var _proto = Slugger.prototype; + + _proto.serialize = function serialize(value) { + return value.toLowerCase().trim() // remove html tags + .replace(/<[!\/a-z].*?>/ig, '') // remove unwanted chars + .replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g, '').replace(/\s/g, '-'); } - case 'heading': { - return this.renderer.heading( - this.inline.output(this.token.text), - this.token.depth, - unescape(this.inlineText.output(this.token.text)), - this.slugger); + /** + * Finds the next safe (unique) slug to use + */ + ; + + _proto.getNextSafeSlug = function getNextSafeSlug(originalSlug, isDryRun) { + var slug = originalSlug; + var occurenceAccumulator = 0; + + if (this.seen.hasOwnProperty(slug)) { + occurenceAccumulator = this.seen[originalSlug]; + + do { + occurenceAccumulator++; + slug = originalSlug + '-' + occurenceAccumulator; + } while (this.seen.hasOwnProperty(slug)); + } + + if (!isDryRun) { + this.seen[originalSlug] = occurenceAccumulator; + this.seen[slug] = 0; + } + + return slug; } - case 'code': { - return this.renderer.code(this.token.text, - this.token.lang, - this.token.escaped); + /** + * Convert string to unique id + * @param {object} options + * @param {boolean} options.dryrun Generates the next unique slug without updating the internal accumulator. + */ + ; + + _proto.slug = function slug(value, options) { + if (options === void 0) { + options = {}; + } + + var slug = this.serialize(value); + return this.getNextSafeSlug(slug, options.dryrun); + }; + + return Slugger; + }(); + + var defaults$4 = defaults.defaults; + var unescape$1 = helpers.unescape; + /** + * Parsing & Compiling + */ + + var Parser_1 = /*#__PURE__*/function () { + function Parser(options) { + this.options = options || defaults$4; + this.options.renderer = this.options.renderer || new Renderer_1(); + this.renderer = this.options.renderer; + this.renderer.options = this.options; + this.textRenderer = new TextRenderer_1(); + this.slugger = new Slugger_1(); } - case 'table': { - var header = '', - body = '', + /** + * Static Parse Method + */ + + + Parser.parse = function parse(tokens, options) { + var parser = new Parser(options); + return parser.parse(tokens); + } + /** + * Parse Loop + */ + ; + + var _proto = Parser.prototype; + + _proto.parse = function parse(tokens, top) { + if (top === void 0) { + top = true; + } + + var out = '', i, + j, + k, + l2, + l3, row, cell, - j; + header, + body, + token, + ordered, + start, + loose, + itemBody, + item, + checked, + task, + checkbox; + var l = tokens.length; - // header - cell = ''; - for (i = 0; i < this.token.header.length; i++) { - cell += this.renderer.tablecell( - this.inline.output(this.token.header[i]), - { header: true, align: this.token.align[i] } - ); - } - header += this.renderer.tablerow(cell); + for (i = 0; i < l; i++) { + token = tokens[i]; - for (i = 0; i < this.token.cells.length; i++) { - row = this.token.cells[i]; + switch (token.type) { + case 'space': + { + continue; + } - cell = ''; - for (j = 0; j < row.length; j++) { - cell += this.renderer.tablecell( - this.inline.output(row[j]), - { header: false, align: this.token.align[j] } - ); + case 'hr': + { + out += this.renderer.hr(); + continue; + } + + case 'heading': + { + out += this.renderer.heading(this.parseInline(token.tokens), token.depth, unescape$1(this.parseInline(token.tokens, this.textRenderer)), this.slugger); + continue; + } + + case 'code': + { + out += this.renderer.code(token.text, token.lang, token.escaped); + continue; + } + + case 'table': + { + header = ''; // header + + cell = ''; + l2 = token.header.length; + + for (j = 0; j < l2; j++) { + cell += this.renderer.tablecell(this.parseInline(token.tokens.header[j]), { + header: true, + align: token.align[j] + }); + } + + header += this.renderer.tablerow(cell); + body = ''; + l2 = token.cells.length; + + for (j = 0; j < l2; j++) { + row = token.tokens.cells[j]; + cell = ''; + l3 = row.length; + + for (k = 0; k < l3; k++) { + cell += this.renderer.tablecell(this.parseInline(row[k]), { + header: false, + align: token.align[k] + }); + } + + body += this.renderer.tablerow(cell); + } + + out += this.renderer.table(header, body); + continue; + } + + case 'blockquote': + { + body = this.parse(token.tokens); + out += this.renderer.blockquote(body); + continue; + } + + case 'list': + { + ordered = token.ordered; + start = token.start; + loose = token.loose; + l2 = token.items.length; + body = ''; + + for (j = 0; j < l2; j++) { + item = token.items[j]; + checked = item.checked; + task = item.task; + itemBody = ''; + + if (item.task) { + checkbox = this.renderer.checkbox(checked); + + if (loose) { + if (item.tokens.length > 0 && item.tokens[0].type === 'text') { + item.tokens[0].text = checkbox + ' ' + item.tokens[0].text; + + if (item.tokens[0].tokens && item.tokens[0].tokens.length > 0 && item.tokens[0].tokens[0].type === 'text') { + item.tokens[0].tokens[0].text = checkbox + ' ' + item.tokens[0].tokens[0].text; + } + } else { + item.tokens.unshift({ + type: 'text', + text: checkbox + }); + } + } else { + itemBody += checkbox; + } + } + + itemBody += this.parse(item.tokens, loose); + body += this.renderer.listitem(itemBody, task, checked); + } + + out += this.renderer.list(body, ordered, start); + continue; + } + + case 'html': + { + // TODO parse inline content if parameter markdown=1 + out += this.renderer.html(token.text); + continue; + } + + case 'paragraph': + { + out += this.renderer.paragraph(this.parseInline(token.tokens)); + continue; + } + + case 'text': + { + body = token.tokens ? this.parseInline(token.tokens) : token.text; + + while (i + 1 < l && tokens[i + 1].type === 'text') { + token = tokens[++i]; + body += '\n' + (token.tokens ? this.parseInline(token.tokens) : token.text); + } + + out += top ? this.renderer.paragraph(body) : body; + continue; + } + + default: + { + var errMsg = 'Token with "' + token.type + '" type was not found.'; + + if (this.options.silent) { + console.error(errMsg); + return; + } else { + throw new Error(errMsg); + } + } } - - body += this.renderer.tablerow(cell); - } - return this.renderer.table(header, body); - } - case 'blockquote_start': { - body = ''; - - while (this.next().type !== 'blockquote_end') { - body += this.tok(); } - return this.renderer.blockquote(body); + return out; } - case 'list_start': { - body = ''; - var ordered = this.token.ordered, - start = this.token.start; + /** + * Parse Inline Tokens + */ + ; - while (this.next().type !== 'list_end') { - body += this.tok(); - } + _proto.parseInline = function parseInline(tokens, renderer) { + renderer = renderer || this.renderer; + var out = '', + i, + token; + var l = tokens.length; - return this.renderer.list(body, ordered, start); - } - case 'list_item_start': { - body = ''; - var loose = this.token.loose; - var checked = this.token.checked; - var task = this.token.task; + for (i = 0; i < l; i++) { + token = tokens[i]; - if (this.token.task) { - body += this.renderer.checkbox(checked); - } + switch (token.type) { + case 'escape': + { + out += renderer.text(token.text); + break; + } - while (this.next().type !== 'list_item_end') { - body += !loose && this.token.type === 'text' - ? this.parseText() - : this.tok(); - } - return this.renderer.listitem(body, task, checked); - } - case 'html': { - // TODO parse inline content if parameter markdown=1 - return this.renderer.html(this.token.text); - } - case 'paragraph': { - return this.renderer.paragraph(this.inline.output(this.token.text)); - } - case 'text': { - return this.renderer.paragraph(this.parseText()); - } - default: { - var errMsg = 'Token with "' + this.token.type + '" type was not found.'; - if (this.options.silent) { - console.log(errMsg); - } else { - throw new Error(errMsg); - } - } - } -}; + case 'html': + { + out += renderer.html(token.text); + break; + } -/** - * Slugger generates header id - */ + case 'link': + { + out += renderer.link(token.href, token.title, this.parseInline(token.tokens, renderer)); + break; + } -function Slugger () { - this.seen = {}; -} + case 'image': + { + out += renderer.image(token.href, token.title, token.text); + break; + } -/** - * Convert string to unique id - */ + case 'strong': + { + out += renderer.strong(this.parseInline(token.tokens, renderer)); + break; + } -Slugger.prototype.slug = function (value) { - var slug = value - .toLowerCase() - .trim() - .replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g, '') - .replace(/\s/g, '-'); + case 'em': + { + out += renderer.em(this.parseInline(token.tokens, renderer)); + break; + } - if (this.seen.hasOwnProperty(slug)) { - var originalSlug = slug; - do { - this.seen[originalSlug]++; - slug = originalSlug + '-' + this.seen[originalSlug]; - } while (this.seen.hasOwnProperty(slug)); - } - this.seen[slug] = 0; + case 'codespan': + { + out += renderer.codespan(token.text); + break; + } - return slug; -}; + case 'br': + { + out += renderer.br(); + break; + } -/** - * Helpers - */ + case 'del': + { + out += renderer.del(this.parseInline(token.tokens, renderer)); + break; + } -function escape(html, encode) { - if (encode) { - if (escape.escapeTest.test(html)) { - return html.replace(escape.escapeReplace, function (ch) { return escape.replacements[ch]; }); - } - } else { - if (escape.escapeTestNoEncode.test(html)) { - return html.replace(escape.escapeReplaceNoEncode, function (ch) { return escape.replacements[ch]; }); - } - } + case 'text': + { + out += renderer.text(token.text); + break; + } - return html; -} + default: + { + var errMsg = 'Token with "' + token.type + '" type was not found.'; -escape.escapeTest = /[&<>"']/; -escape.escapeReplace = /[&<>"']/g; -escape.replacements = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''' -}; - -escape.escapeTestNoEncode = /[<>"']|&(?!#?\w+;)/; -escape.escapeReplaceNoEncode = /[<>"']|&(?!#?\w+;)/g; - -function unescape(html) { - // explicitly match decimal, hex, and named HTML entities - return html.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig, function(_, n) { - n = n.toLowerCase(); - if (n === 'colon') return ':'; - if (n.charAt(0) === '#') { - return n.charAt(1) === 'x' - ? String.fromCharCode(parseInt(n.substring(2), 16)) - : String.fromCharCode(+n.substring(1)); - } - return ''; - }); -} - -function edit(regex, opt) { - regex = regex.source || regex; - opt = opt || ''; - return { - replace: function(name, val) { - val = val.source || val; - val = val.replace(/(^|[^\[])\^/g, '$1'); - regex = regex.replace(name, val); - return this; - }, - getRegex: function() { - return new RegExp(regex, opt); - } - }; -} - -function cleanUrl(sanitize, base, href) { - if (sanitize) { - try { - var prot = decodeURIComponent(unescape(href)) - .replace(/[^\w:]/g, '') - .toLowerCase(); - } catch (e) { - return null; - } - if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) { - return null; - } - } - if (base && !originIndependentUrl.test(href)) { - href = resolveUrl(base, href); - } - try { - href = encodeURI(href).replace(/%25/g, '%'); - } catch (e) { - return null; - } - return href; -} - -function resolveUrl(base, href) { - if (!baseUrls[' ' + base]) { - // we can ignore everything in base after the last slash of its path component, - // but we might need to add _that_ - // https://tools.ietf.org/html/rfc3986#section-3 - if (/^[^:]+:\/*[^/]*$/.test(base)) { - baseUrls[' ' + base] = base + '/'; - } else { - baseUrls[' ' + base] = rtrim(base, '/', true); - } - } - base = baseUrls[' ' + base]; - - if (href.slice(0, 2) === '//') { - return base.replace(/:[\s\S]*/, ':') + href; - } else if (href.charAt(0) === '/') { - return base.replace(/(:\/*[^/]*)[\s\S]*/, '$1') + href; - } else { - return base + href; - } -} -var baseUrls = {}; -var originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i; - -function noop() {} -noop.exec = noop; - -function merge(obj) { - var i = 1, - target, - key; - - for (; i < arguments.length; i++) { - target = arguments[i]; - for (key in target) { - if (Object.prototype.hasOwnProperty.call(target, key)) { - obj[key] = target[key]; - } - } - } - - return obj; -} - -function splitCells(tableRow, count) { - // ensure that every cell-delimiting pipe has a space - // before it to distinguish it from an escaped pipe - var row = tableRow.replace(/\|/g, function (match, offset, str) { - var escaped = false, - curr = offset; - while (--curr >= 0 && str[curr] === '\\') escaped = !escaped; - if (escaped) { - // odd number of slashes means | is escaped - // so we leave it alone - return '|'; - } else { - // add space before unescaped | - return ' |'; + if (this.options.silent) { + console.error(errMsg); + return; + } else { + throw new Error(errMsg); + } + } } - }), - cells = row.split(/ \|/), - i = 0; - - if (cells.length > count) { - cells.splice(count); - } else { - while (cells.length < count) cells.push(''); - } - - for (; i < cells.length; i++) { - // leading or trailing whitespace is ignored per the gfm spec - cells[i] = cells[i].trim().replace(/\\\|/g, '|'); - } - return cells; -} - -// Remove trailing 'c's. Equivalent to str.replace(/c*$/, ''). -// /c*$/ is vulnerable to REDOS. -// invert: Remove suffix of non-c chars instead. Default falsey. -function rtrim(str, c, invert) { - if (str.length === 0) { - return ''; - } - - // Length of suffix matching the invert condition. - var suffLen = 0; - - // Step left until we fail to match the invert condition. - while (suffLen < str.length) { - var currChar = str.charAt(str.length - suffLen - 1); - if (currChar === c && !invert) { - suffLen++; - } else if (currChar !== c && invert) { - suffLen++; - } else { - break; - } - } - - return str.substr(0, str.length - suffLen); -} - -function findClosingBracket(str, b) { - if (str.indexOf(b[1]) === -1) { - return -1; - } - var level = 0; - for (var i = 0; i < str.length; i++) { - if (str[i] === '\\') { - i++; - } else if (str[i] === b[0]) { - level++; - } else if (str[i] === b[1]) { - level--; - if (level < 0) { - return i; } + + return out; + }; + + return Parser; + }(); + + var merge$2 = helpers.merge, + checkSanitizeDeprecation$1 = helpers.checkSanitizeDeprecation, + escape$2 = helpers.escape; + var getDefaults = defaults.getDefaults, + changeDefaults = defaults.changeDefaults, + defaults$5 = defaults.defaults; + /** + * Marked + */ + + function marked(src, opt, callback) { + // throw error in case of non string input + if (typeof src === 'undefined' || src === null) { + throw new Error('marked(): input parameter is undefined or null'); } - } - return -1; -} -/** - * Marked - */ + if (typeof src !== 'string') { + throw new Error('marked(): input parameter is of type ' + Object.prototype.toString.call(src) + ', string expected'); + } -function marked(src, opt, callback) { - // throw error in case of non string input - if (typeof src === 'undefined' || src === null) { - throw new Error('marked(): input parameter is undefined or null'); - } - if (typeof src !== 'string') { - throw new Error('marked(): input parameter is of type ' - + Object.prototype.toString.call(src) + ', string expected'); - } - - if (callback || typeof opt === 'function') { - if (!callback) { + if (typeof opt === 'function') { callback = opt; opt = null; } - opt = merge({}, marked.defaults, opt || {}); + opt = merge$2({}, marked.defaults, opt || {}); + checkSanitizeDeprecation$1(opt); - var highlight = opt.highlight, - tokens, - pending, - i = 0; - - try { - tokens = Lexer.lex(src, opt); - } catch (e) { - return callback(e); - } - - pending = tokens.length; - - var done = function(err) { - if (err) { - opt.highlight = highlight; - return callback(err); - } - - var out; + if (callback) { + var highlight = opt.highlight; + var tokens; try { - out = Parser.parse(tokens, opt); + tokens = Lexer_1.lex(src, opt); } catch (e) { - err = e; + return callback(e); } - opt.highlight = highlight; + var done = function done(err) { + var out; - return err - ? callback(err) - : callback(null, out); - }; - - if (!highlight || highlight.length < 3) { - return done(); - } - - delete opt.highlight; - - if (!pending) return done(); - - for (; i < tokens.length; i++) { - (function(token) { - if (token.type !== 'code') { - return --pending || done(); - } - return highlight(token.text, token.lang, function(err, code) { - if (err) return done(err); - if (code == null || code === token.text) { - return --pending || done(); + if (!err) { + try { + out = Parser_1.parse(tokens, opt); + } catch (e) { + err = e; } - token.text = code; - token.escaped = true; - --pending || done(); - }); - })(tokens[i]); + } + + opt.highlight = highlight; + return err ? callback(err) : callback(null, out); + }; + + if (!highlight || highlight.length < 3) { + return done(); + } + + delete opt.highlight; + if (!tokens.length) return done(); + var pending = 0; + marked.walkTokens(tokens, function (token) { + if (token.type === 'code') { + pending++; + setTimeout(function () { + highlight(token.text, token.lang, function (err, code) { + if (err) { + return done(err); + } + + if (code != null && code !== token.text) { + token.text = code; + token.escaped = true; + } + + pending--; + + if (pending === 0) { + done(); + } + }); + }, 0); + } + }); + + if (pending === 0) { + done(); + } + + return; } - return; - } - try { - if (opt) opt = merge({}, marked.defaults, opt); - return Parser.parse(Lexer.lex(src, opt), opt); - } catch (e) { - e.message += '\nPlease report this to https://github.com/markedjs/marked.'; - if ((opt || marked.defaults).silent) { - return '

    An error occurred:

    '
    -        + escape(e.message + '', true)
    -        + '
    '; + try { + var _tokens = Lexer_1.lex(src, opt); + + if (opt.walkTokens) { + marked.walkTokens(_tokens, opt.walkTokens); + } + + return Parser_1.parse(_tokens, opt); + } catch (e) { + e.message += '\nPlease report this to https://github.com/markedjs/marked.'; + + if (opt.silent) { + return '

    An error occurred:

    ' + escape$2(e.message + '', true) + '
    '; + } + + throw e; } - throw e; } -} + /** + * Options + */ -/** - * Options - */ -marked.options = -marked.setOptions = function(opt) { - merge(marked.defaults, opt); - return marked; -}; - -marked.getDefaults = function () { - return { - baseUrl: null, - breaks: false, - gfm: true, - headerIds: true, - headerPrefix: '', - highlight: null, - langPrefix: 'language-', - mangle: true, - pedantic: false, - renderer: new Renderer(), - sanitize: false, - sanitizer: null, - silent: false, - smartLists: false, - smartypants: false, - tables: true, - xhtml: false + marked.options = marked.setOptions = function (opt) { + merge$2(marked.defaults, opt); + changeDefaults(marked.defaults); + return marked; }; -}; -marked.defaults = marked.getDefaults(); + marked.getDefaults = getDefaults; + marked.defaults = defaults$5; + /** + * Use Extension + */ -/** - * Expose - */ + marked.use = function (extension) { + var opts = merge$2({}, extension); -marked.Parser = Parser; -marked.parser = Parser.parse; + if (extension.renderer) { + (function () { + var renderer = marked.defaults.renderer || new Renderer_1(); -marked.Renderer = Renderer; -marked.TextRenderer = TextRenderer; + var _loop = function _loop(prop) { + var prevRenderer = renderer[prop]; -marked.Lexer = Lexer; -marked.lexer = Lexer.lex; + renderer[prop] = function () { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } -marked.InlineLexer = InlineLexer; -marked.inlineLexer = InlineLexer.output; + var ret = extension.renderer[prop].apply(renderer, args); -marked.Slugger = Slugger; + if (ret === false) { + ret = prevRenderer.apply(renderer, args); + } -marked.parse = marked; + return ret; + }; + }; -// BEGIN MONACOCHANGE -// if (typeof module !== 'undefined' && typeof exports === 'object') { -// module.exports = marked; -// } else if (typeof define === 'function' && define.amd) { -// define(function() { return marked; }); -// } else { -// root.marked = marked; -// } -// })(this || (typeof window !== 'undefined' ? window : global)); -__marked_exports = marked; -}).call(this); + for (var prop in extension.renderer) { + _loop(prop); + } -// ESM-comment-begin -define(function() { return __marked_exports; }); -// ESM-comment-end + opts.renderer = renderer; + })(); + } -// ESM-uncomment-begin -// export var marked = __marked_exports; -// export var Parser = __marked_exports.Parser; -// export var parser = __marked_exports.parser; -// export var Renderer = __marked_exports.Renderer; -// export var TextRenderer = __marked_exports.TextRenderer; -// export var Lexer = __marked_exports.Lexer; -// export var lexer = __marked_exports.lexer; -// export var InlineLexer = __marked_exports.InlineLexer; -// export var inlineLexer = __marked_exports.inlineLexer; -// export var parse = __marked_exports.parse; -// ESM-uncomment-end -// END MONACOCHANGE + if (extension.tokenizer) { + (function () { + var tokenizer = marked.defaults.tokenizer || new Tokenizer_1(); + + var _loop2 = function _loop2(prop) { + var prevTokenizer = tokenizer[prop]; + + tokenizer[prop] = function () { + for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + + var ret = extension.tokenizer[prop].apply(tokenizer, args); + + if (ret === false) { + ret = prevTokenizer.apply(tokenizer, args); + } + + return ret; + }; + }; + + for (var prop in extension.tokenizer) { + _loop2(prop); + } + + opts.tokenizer = tokenizer; + })(); + } + + if (extension.walkTokens) { + var walkTokens = marked.defaults.walkTokens; + + opts.walkTokens = function (token) { + extension.walkTokens(token); + + if (walkTokens) { + walkTokens(token); + } + }; + } + + marked.setOptions(opts); + }; + /** + * Run callback for every token + */ + + + marked.walkTokens = function (tokens, callback) { + for (var _iterator = _createForOfIteratorHelperLoose(tokens), _step; !(_step = _iterator()).done;) { + var token = _step.value; + callback(token); + + switch (token.type) { + case 'table': + { + for (var _iterator2 = _createForOfIteratorHelperLoose(token.tokens.header), _step2; !(_step2 = _iterator2()).done;) { + var cell = _step2.value; + marked.walkTokens(cell, callback); + } + + for (var _iterator3 = _createForOfIteratorHelperLoose(token.tokens.cells), _step3; !(_step3 = _iterator3()).done;) { + var row = _step3.value; + + for (var _iterator4 = _createForOfIteratorHelperLoose(row), _step4; !(_step4 = _iterator4()).done;) { + var _cell = _step4.value; + marked.walkTokens(_cell, callback); + } + } + + break; + } + + case 'list': + { + marked.walkTokens(token.items, callback); + break; + } + + default: + { + if (token.tokens) { + marked.walkTokens(token.tokens, callback); + } + } + } + } + }; + /** + * Expose + */ + + + marked.Parser = Parser_1; + marked.parser = Parser_1.parse; + marked.Renderer = Renderer_1; + marked.TextRenderer = TextRenderer_1; + marked.Lexer = Lexer_1; + marked.lexer = Lexer_1.lex; + marked.Tokenizer = Tokenizer_1; + marked.Slugger = Slugger_1; + marked.parse = marked; + var marked_1 = marked; + + return marked_1; + +}))); diff --git a/src/vs/base/common/marshalling.ts b/src/vs/base/common/marshalling.ts index e76ba91738f..4af920357fd 100644 --- a/src/vs/base/common/marshalling.ts +++ b/src/vs/base/common/marshalling.ts @@ -5,7 +5,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { regExpFlags } from 'vs/base/common/strings'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; export function stringify(obj: any): string { return JSON.stringify(obj, replacer); @@ -33,7 +33,15 @@ function replacer(key: string, value: any): any { return value; } -export function revive(obj: any, depth = 0): any { + +type Deserialize = T extends UriComponents ? URI + : T extends object + ? Revived + : T; + +export type Revived = { [K in keyof T]: Deserialize }; + +export function revive(obj: any, depth = 0): Revived { if (!obj || depth > 200) { return obj; } @@ -41,15 +49,15 @@ export function revive(obj: any, depth = 0): any { if (typeof obj === 'object') { switch ((obj).$mid) { - case 1: return URI.revive(obj); - case 2: return new RegExp(obj.source, obj.flags); + case 1: return URI.revive(obj); + case 2: return new RegExp(obj.source, obj.flags); } if ( obj instanceof VSBuffer || obj instanceof Uint8Array ) { - return obj; + return obj; } if (Array.isArray(obj)) { diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index 1286c5117a4..4b6aebc1646 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -58,6 +58,8 @@ export namespace Schemas { export const vscodeNotebook = 'vscode-notebook'; + export const vscodeNotebookCell = 'vscode-notebook-cell'; + export const vscodeSettings = 'vscode-settings'; export const webviewPanel = 'webview-panel'; diff --git a/src/vs/base/common/stream.ts b/src/vs/base/common/stream.ts index 7ed2befbe0d..260e13f02b3 100644 --- a/src/vs/base/common/stream.ts +++ b/src/vs/base/common/stream.ts @@ -33,7 +33,7 @@ export interface ReadableStreamEvents { /** * A interface that emulates the API shape of a node.js readable - * stream for use in desktop and web environments. + * stream for use in native and web environments. */ export interface ReadableStream extends ReadableStreamEvents { @@ -60,7 +60,7 @@ export interface ReadableStream extends ReadableStreamEvents { /** * A interface that emulates the API shape of a node.js readable - * for use in desktop and web environments. + * for use in native and web environments. */ export interface Readable { @@ -73,7 +73,7 @@ export interface Readable { /** * A interface that emulates the API shape of a node.js writeable - * stream for use in desktop and web environments. + * stream for use in native and web environments. */ export interface WriteableStream extends ReadableStream { diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 5003acabe09..ec727797b87 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -855,10 +855,6 @@ export function stripUTF8BOM(str: string): string { return startsWithUTF8BOM(str) ? str.substr(1) : str; } -export function safeBtoa(str: string): string { - return btoa(encodeURIComponent(str)); // we use encodeURIComponent because btoa fails for non Latin 1 values -} - /** * @deprecated ES6 */ diff --git a/src/vs/base/common/types.ts b/src/vs/base/common/types.ts index ee9172a4f84..eef27600bd7 100644 --- a/src/vs/base/common/types.ts +++ b/src/vs/base/common/types.ts @@ -258,14 +258,12 @@ export type UriDto = { [K in keyof T]: T[K] extends URI /** * Mapped-type that replaces all occurrences of URI with UriComponents and * drops all functions. - * todo@joh use toJSON-results */ -export type Dto = { [K in keyof T]: T[K] extends URI - ? UriComponents - : T[K] extends Function - ? never - : UriDto }; - +export type Dto = T extends { toJSON(): infer U } + ? U + : T extends object + ? { [k in keyof T]: Dto; } + : T; export function NotImplementedProxy(name: string): { new(): T } { return class { diff --git a/src/vs/base/node/ps.ts b/src/vs/base/node/ps.ts index 55c3a73da57..0786b753dd2 100644 --- a/src/vs/base/node/ps.ts +++ b/src/vs/base/node/ps.ts @@ -219,7 +219,8 @@ export function listProcesses(rootPid: number): Promise { // Set numeric locale to ensure '.' is used as the decimal separator exec(`${ps} ${args}`, { maxBuffer: 1000 * 1024, env: { LC_NUMERIC: 'en_US.UTF-8' } }, (err, stdout, stderr) => { - if (err || stderr) { + // Silently ignoring the screen size is bogus error. See https://github.com/microsoft/vscode/issues/98590 + if (err || (stderr && !stderr.includes('screen size is bogus'))) { reject(err || new Error(stderr.toString())); } else { parsePsOutput(stdout, addToTree); @@ -246,4 +247,4 @@ function parsePsOutput(stdout: string, addToTree: (pid: number, ppid: number, cm addToTree(parseInt(matches[1]), parseInt(matches[2]), matches[5], parseFloat(matches[3]), parseFloat(matches[4])); } } -} \ No newline at end of file +} diff --git a/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts b/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts index 6222be78028..a7fbfc81e6b 100644 --- a/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts +++ b/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts @@ -5,13 +5,14 @@ import { Menu, MenuItem, BrowserWindow, ipcMain, IpcMainEvent } from 'electron'; import { ISerializableContextMenuItem, CONTEXT_MENU_CLOSE_CHANNEL, CONTEXT_MENU_CHANNEL, IPopupOptions } from 'vs/base/parts/contextmenu/common/contextmenu'; +import { withNullAsUndefined } from 'vs/base/common/types'; export function registerContextMenuListener(): void { ipcMain.on(CONTEXT_MENU_CHANNEL, (event: IpcMainEvent, contextMenuId: number, items: ISerializableContextMenuItem[], onClickChannel: string, options?: IPopupOptions) => { const menu = createMenu(event, onClickChannel, items); menu.popup({ - window: BrowserWindow.fromWebContents(event.sender), + window: withNullAsUndefined(BrowserWindow.fromWebContents(event.sender)), x: options ? options.x : undefined, y: options ? options.y : undefined, positioningItem: options ? options.positioningItem : undefined, diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index 25475e8e20d..d2ca01cf849 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -278,7 +278,7 @@ class QuickInput extends Disposable implements IQuickInput { if (title && this.ui.title.textContent !== title) { this.ui.title.textContent = title; } else if (!title && this.ui.title.innerHTML !== ' ') { - this.ui.title.innerHTML = ' '; + this.ui.title.innerText = '\u00a0;'; } const description = this.getDescription(); if (this.ui.description.textContent !== description) { @@ -381,7 +381,7 @@ class QuickPick extends QuickInput implements IQuickPi private static readonly DEFAULT_ARIA_LABEL = localize('quickInputBox.ariaLabel', "Type to narrow down results."); private _value = ''; - private _ariaLabel = QuickPick.DEFAULT_ARIA_LABEL; + private _ariaLabel: string | undefined; private _placeholder: string | undefined; private readonly onDidChangeValueEmitter = this._register(new Emitter()); private readonly onDidAcceptEmitter = this._register(new Emitter()); @@ -435,8 +435,8 @@ class QuickPick extends QuickInput implements IQuickPi filterValue = (value: string) => value; - set ariaLabel(ariaLabel: string) { - this._ariaLabel = ariaLabel || QuickPick.DEFAULT_ARIA_LABEL; + set ariaLabel(ariaLabel: string | undefined) { + this._ariaLabel = ariaLabel; this.update(); } @@ -884,8 +884,11 @@ class QuickPick extends QuickInput implements IQuickPi } if (inputShownJustForScreenReader) { this.ui.inputBox.ariaLabel = ''; - } else if (this.ui.inputBox.ariaLabel !== this.ariaLabel) { - this.ui.inputBox.ariaLabel = this.ariaLabel; + } else { + const ariaLabel = this.ariaLabel || this.placeholder || QuickPick.DEFAULT_ARIA_LABEL; + if (this.ui.inputBox.ariaLabel !== ariaLabel) { + this.ui.inputBox.ariaLabel = ariaLabel; + } } this.ui.list.matchOnDescription = this.matchOnDescription; this.ui.list.matchOnDetail = this.matchOnDetail; @@ -1384,9 +1387,6 @@ export class QuickInputController extends Disposable { ]; input.canSelectMany = !!options.canPickMany; input.placeholder = options.placeHolder; - if (options.placeHolder) { - input.ariaLabel = options.placeHolder; - } input.ignoreFocusOut = !!options.ignoreFocusLost; input.matchOnDescription = !!options.matchOnDescription; input.matchOnDetail = !!options.matchOnDetail; diff --git a/src/vs/base/parts/quickinput/common/quickInput.ts b/src/vs/base/parts/quickinput/common/quickInput.ts index ebad819fffa..de7339e6757 100644 --- a/src/vs/base/parts/quickinput/common/quickInput.ts +++ b/src/vs/base/parts/quickinput/common/quickInput.ts @@ -199,7 +199,7 @@ export interface IQuickPick extends IQuickInput { */ filterValue: (value: string) => string; - ariaLabel: string; + ariaLabel: string | undefined; placeholder: string | undefined; diff --git a/src/vs/base/parts/sandbox/common/electronTypes.ts b/src/vs/base/parts/sandbox/common/electronTypes.ts index 8a5f4120862..c7729f338af 100644 --- a/src/vs/base/parts/sandbox/common/electronTypes.ts +++ b/src/vs/base/parts/sandbox/common/electronTypes.ts @@ -209,37 +209,6 @@ export interface SaveDialogReturnValue { bookmark?: string; } -export interface CrashReporterStartOptions { - companyName: string; - /** - * URL that crash reports will be sent to as POST. - */ - submitURL: string; - /** - * Defaults to `app.name`. - */ - productName?: string; - /** - * Whether crash reports should be sent to the server. Default is `true`. - */ - uploadToServer?: boolean; - /** - * Default is `false`. - */ - ignoreSystemCrashHandler?: boolean; - /** - * An object you can define that will be sent along with the report. Only string - * properties are sent correctly. Nested objects are not supported. When using - * Windows, the property names and values must be fewer than 64 characters. - */ - extra?: Record; - /** - * Directory to store the crash reports temporarily (only used when the crash - * reporter is started via `process.crashReporter.start`). - */ - crashesDirectory?: string; -} - export interface FileFilter { // Docs: http://electronjs.org/docs/api/structures/file-filter @@ -281,3 +250,62 @@ export interface MouseInputEvent extends InputEvent { x: number; y: number; } + +export interface CrashReporterStartOptions { + /** + * URL that crash reports will be sent to as POST. + */ + submitURL: string; + /** + * Defaults to `app.name`. + */ + productName?: string; + /** + * Deprecated alias for `{ globalExtra: { _companyName: ... } }`. + * + * @deprecated + */ + companyName?: string; + /** + * Whether crash reports should be sent to the server. If false, crash reports will + * be collected and stored in the crashes directory, but not uploaded. Default is + * `true`. + */ + uploadToServer?: boolean; + /** + * If true, crashes generated in the main process will not be forwarded to the + * system crash handler. Default is `false`. + */ + ignoreSystemCrashHandler?: boolean; + /** + * If true, limit the number of crashes uploaded to 1/hour. Default is `false`. + * + * @platform darwin,win32 + */ + rateLimit?: boolean; + /** + * If true, crash reports will be compressed and uploaded with `Content-Encoding: + * gzip`. Not all collection servers support compressed payloads. Default is + * `false`. + * + * @platform darwin,win32 + */ + compress?: boolean; + /** + * Extra string key/value annotations that will be sent along with crash reports + * that are generated in the main process. Only string values are supported. + * Crashes generated in child processes will not contain these extra parameters to + * crash reports generated from child processes, call `addExtraParameter` from the + * child process. + */ + extra?: Record; + /** + * Extra string key/value annotations that will be sent along with any crash + * reports generated in any process. These annotations cannot be changed once the + * crash reporter has been started. If a key is present in both the global extra + * parameters and the process-specific extra parameters, then the global one will + * take precedence. By default, `productName` and the app version are included, as + * well as the Electron version. + */ + globalExtra?: Record; +} diff --git a/src/vs/base/parts/sandbox/electron-browser/preload.js b/src/vs/base/parts/sandbox/electron-browser/preload.js index d10c4be3ae1..d6bea974701 100644 --- a/src/vs/base/parts/sandbox/electron-browser/preload.js +++ b/src/vs/base/parts/sandbox/electron-browser/preload.js @@ -74,15 +74,16 @@ }, /** - * Support for subset of methods of Electron's `crashReporter` type. + * Support for subset of methods of Electron's `crashReporter` type. */ crashReporter: { /** - * @param {Electron.CrashReporterStartOptions} options + * @param {string} key + * @param {string} value */ - start(options) { - crashReporter.start(options); + addExtraParameter(key, value) { + crashReporter.addExtraParameter(key, value); } }, @@ -92,6 +93,14 @@ process: { platform: process.platform, env: process.env, + _whenEnvResolved: undefined, + get whenEnvResolved() { + if (!this._whenEnvResolved) { + this._whenEnvResolved = resolveEnv(); + } + + return this._whenEnvResolved; + }, on: /** * @param {string} type @@ -156,5 +165,33 @@ return true; } + /** + * If VSCode is not run from a terminal, we should resolve additional + * shell specific environment from the OS shell to ensure we are seeing + * all development related environment variables. We do this from the + * main process because it may involve spawning a shell. + */ + function resolveEnv() { + return new Promise(function (resolve) { + const handle = setTimeout(function () { + console.warn('Preload: Unable to resolve shell environment in a reasonable time'); + + // It took too long to fetch the shell environment, return + resolve(); + }, 3000); + + ipcRenderer.once('vscode:acceptShellEnv', function (event, shellEnv) { + clearTimeout(handle); + + // Assign all keys of the shell environment to our process environment + Object.assign(process.env, shellEnv); + + resolve(); + }); + + ipcRenderer.send('vscode:fetchShellEnv'); + }); + } + //#endregion }()); diff --git a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts index 573e4735933..a305df1c8a3 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CrashReporterStartOptions } from 'vs/base/parts/sandbox/common/electronTypes'; - export const ipcRenderer = (window as any).vscode.ipcRenderer as { /** @@ -54,32 +52,23 @@ export const webFrame = (window as any).vscode.webFrame as { export const crashReporter = (window as any).vscode.crashReporter as { /** - * You are required to call this method before using any other `crashReporter` APIs - * and in each process (main/renderer) from which you want to collect crash - * reports. You can pass different options to `crashReporter.start` when calling - * from different processes. + * Set an extra parameter to be sent with the crash report. The values specified + * here will be sent in addition to any values set via the `extra` option when + * `start` was called. * - * **Note** Child processes created via the `child_process` module will not have - * access to the Electron modules. Therefore, to collect crash reports from them, - * use `process.crashReporter.start` instead. Pass the same options as above along - * with an additional one called `crashesDirectory` that should point to a - * directory to store the crash reports temporarily. You can test this out by - * calling `process.crash()` to crash the child process. + * Parameters added in this fashion (or via the `extra` parameter to + * `crashReporter.start`) are specific to the calling process. Adding extra + * parameters in the main process will not cause those parameters to be sent along + * with crashes from renderer or other child processes. Similarly, adding extra + * parameters in a renderer process will not result in those parameters being sent + * with crashes that occur in other renderer processes or in the main process. * - * **Note:** If you need send additional/updated `extra` parameters after your - * first call `start` you can call `addExtraParameter` on macOS or call `start` - * again with the new/updated `extra` parameters on Linux and Windows. - * - * **Note:** On macOS and windows, Electron uses a new `crashpad` client for crash - * collection and reporting. If you want to enable crash reporting, initializing - * `crashpad` from the main process using `crashReporter.start` is required - * regardless of which process you want to collect crashes from. Once initialized - * this way, the crashpad handler collects crashes from all processes. You still - * have to call `crashReporter.start` from the renderer or child process, otherwise - * crashes from them will get reported without `companyName`, `productName` or any - * of the `extra` information. + * **Note:** Parameters have limits on the length of the keys and values. Key names + * must be no longer than 39 bytes, and values must be no longer than 127 bytes. + * Keys with names longer than the maximum will be silently ignored. Key values + * longer than the maximum length will be truncated. */ - start(options: CrashReporterStartOptions): void; + addExtraParameter(key: string, value: string): void; }; export const process = (window as any).vscode.process as { @@ -95,6 +84,12 @@ export const process = (window as any).vscode.process as { */ env: { [key: string]: string | undefined }; + /** + * Allows to await resolving the full process environment by checking for the shell environment + * of the OS in certain cases (e.g. when the app is started from the Dock on macOS). + */ + whenEnvResolved: Promise; + /** * A listener on the process. Only a small subset of listener types are allowed. */ diff --git a/src/vs/base/parts/storage/node/storage.ts b/src/vs/base/parts/storage/node/storage.ts index 58a809a1c36..d6eca3ac03f 100644 --- a/src/vs/base/parts/storage/node/storage.ts +++ b/src/vs/base/parts/storage/node/storage.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Database, Statement } from 'vscode-sqlite3'; +import type { Database, Statement } from 'vscode-sqlite3'; import { Event } from 'vs/base/common/event'; import { timeout } from 'vs/base/common/async'; import { mapToString, setToString } from 'vs/base/common/map'; diff --git a/src/vs/base/test/browser/comparers.test.ts b/src/vs/base/test/browser/comparers.test.ts index 90dff8c2835..4c40ea2a065 100644 --- a/src/vs/base/test/browser/comparers.test.ts +++ b/src/vs/base/test/browser/comparers.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { compareFileNames, compareFileExtensions, compareFileNamesNumeric, compareFileExtensionsNumeric } from 'vs/base/common/comparers'; +import { compareFileNames, compareFileExtensions, compareFileNamesDefault, compareFileExtensionsDefault } from 'vs/base/common/comparers'; import * as assert from 'assert'; const compareLocale = (a: string, b: string) => a.localeCompare(b); @@ -15,7 +15,7 @@ suite('Comparers', () => { test('compareFileNames', () => { // - // Comparisons with the same results as compareFileNamesNumeric + // Comparisons with the same results as compareFileNamesDefault // // name-only comparisons @@ -28,6 +28,7 @@ suite('Comparers', () => { // name plus extension comparisons assert(compareFileNames('bbb.aaa', 'aaa.bbb') > 0, 'files with extensions are compared first by filename'); + assert(compareFileNames('aggregate.go', 'aggregate_repo.go') > 0, 'compares the whole name all at once by locale'); // dotfile comparisons assert(compareFileNames('.abc', '.abc') === 0, 'equal dotfile names should be equal'); @@ -52,7 +53,7 @@ suite('Comparers', () => { assert(compareFileNames('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); // - // Comparisons with different results than compareFileNamesNumeric + // Comparisons with different results than compareFileNamesDefault // // name-only comparisons @@ -61,9 +62,6 @@ suite('Comparers', () => { assert.notDeepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNames), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases do not sort in locale order'); assert.notDeepEqual(['email', 'Email', 'Ć©mail', 'Ɖmail'].sort(compareFileNames), ['email', 'Email', 'Ć©mail', 'Ɖmail'].sort(compareLocale), 'the same base characters with different case or accents do not sort in locale order'); - // name plus extension comparisons - assert(compareFileNames('aggregate.go', 'aggregate_repo.go') > 0, 'compares the whole name all at once by locale'); - // numeric comparisons assert(compareFileNames('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort in unicode order'); assert(compareFileNames('abc.txt1', 'abc.txt01') > 0, 'same name plus extensions with equal numbers sort in unicode order'); @@ -75,7 +73,7 @@ suite('Comparers', () => { test('compareFileExtensions', () => { // - // Comparisons with the same results as compareFileExtensionsNumeric + // Comparisons with the same results as compareFileExtensionsDefault // // name-only comparisons @@ -118,12 +116,8 @@ suite('Comparers', () => { assert(compareFileExtensions('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared'); assert(compareFileExtensions('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically'); - // Same extension comparison that has the same result as compareFileExtensionsNumeric, but a different result than compareFileNames - // This is an edge case caused by compareFileNames comparing the whole name all at once instead of the name and then the extension. - assert(compareFileExtensions('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, names sort in dictionary order'); - // - // Comparisons with different results from compareFileExtensionsNumeric + // Comparisons with different results from compareFileExtensionsDefault // // name-only comparisions @@ -135,6 +129,7 @@ suite('Comparers', () => { // name plus extension comparisons assert(compareFileExtensions('a.MD', 'a.md') !== compareLocale('MD', 'md'), 'case differences in extensions do not sort by locale'); assert(compareFileExtensions('a.md', 'A.md') !== compareLocale('a', 'A'), 'case differences in names do not sort by locale'); + assert(compareFileExtensions('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, names sort in dictionary order'); // dotfile comparisons assert(compareFileExtensions('.env', '.aaa.env') < 0, 'a dotfile with an extension is treated as a name plus an extension - equal extensions'); @@ -152,145 +147,139 @@ suite('Comparers', () => { }); - test('compareFileNamesNumeric', () => { + test('compareFileNamesDefault', () => { // // Comparisons with the same results as compareFileNames // // name-only comparisons - assert(compareFileNamesNumeric(null, null) === 0, 'null should be equal'); - assert(compareFileNamesNumeric(null, 'abc') < 0, 'null should be come before real values'); - assert(compareFileNamesNumeric('', '') === 0, 'empty should be equal'); - assert(compareFileNamesNumeric('abc', 'abc') === 0, 'equal names should be equal'); - assert(compareFileNamesNumeric('z', 'A') > 0, 'z comes is after A regardless of case'); - assert(compareFileNamesNumeric('Z', 'a') > 0, 'Z comes after a regardless of case'); + assert(compareFileNamesDefault(null, null) === 0, 'null should be equal'); + assert(compareFileNamesDefault(null, 'abc') < 0, 'null should be come before real values'); + assert(compareFileNamesDefault('', '') === 0, 'empty should be equal'); + assert(compareFileNamesDefault('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileNamesDefault('z', 'A') > 0, 'z comes is after A regardless of case'); + assert(compareFileNamesDefault('Z', 'a') > 0, 'Z comes after a regardless of case'); // name plus extension comparisons - assert(compareFileNamesNumeric('file.ext', 'file.ext') === 0, 'equal full names should be equal'); - assert(compareFileNamesNumeric('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); - assert(compareFileNamesNumeric('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); - assert(compareFileNamesNumeric('bbb.aaa', 'aaa.bbb') > 0, 'files should be compared by names even if extensions compare differently'); + assert(compareFileNamesDefault('file.ext', 'file.ext') === 0, 'equal full names should be equal'); + assert(compareFileNamesDefault('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileNamesDefault('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileNamesDefault('bbb.aaa', 'aaa.bbb') > 0, 'files should be compared by names even if extensions compare differently'); + assert(compareFileNamesDefault('aggregate.go', 'aggregate_repo.go') > 0, 'compares the whole filename in locale order'); // dotfile comparisons - assert(compareFileNamesNumeric('.abc', '.abc') === 0, 'equal dotfile names should be equal'); - assert(compareFileNamesNumeric('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); - assert(compareFileNamesNumeric('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); - assert(compareFileNamesNumeric('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); - assert(compareFileNamesNumeric('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); + assert(compareFileNamesDefault('.abc', '.abc') === 0, 'equal dotfile names should be equal'); + assert(compareFileNamesDefault('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); + assert(compareFileNamesDefault('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileNamesDefault('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileNamesDefault('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); // dotfile vs non-dotfile comparisons - assert(compareFileNamesNumeric(null, '.abc') < 0, 'null should come before dotfiles'); - assert(compareFileNamesNumeric('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); - assert(compareFileNamesNumeric('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); - assert(compareFileNamesNumeric('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); - assert(compareFileNamesNumeric('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + assert(compareFileNamesDefault(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileNamesDefault('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileNamesDefault('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileNamesDefault('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + assert(compareFileNamesDefault('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); // numeric comparisons - assert(compareFileNamesNumeric('1', '1') === 0, 'numerically equal full names should be equal'); - assert(compareFileNamesNumeric('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); - assert(compareFileNamesNumeric('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); - assert(compareFileNamesNumeric('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); - assert(compareFileNamesNumeric('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); - assert(compareFileNamesNumeric('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileNamesDefault('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileNamesDefault('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileNamesDefault('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileNamesDefault('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileNamesDefault('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileNamesDefault('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); // // Comparisons with different results than compareFileNames // // name-only comparisons - assert(compareFileNamesNumeric('a', 'A') === compareLocale('a', 'A'), 'the same letter sorts by locale'); - assert(compareFileNamesNumeric('Ć¢', 'Ƃ') === compareLocale('Ć¢', 'Ƃ'), 'the same accented letter sorts by locale'); - assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesNumeric), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order'); - assert.deepEqual(['email', 'Email', 'Ć©mail', 'Ɖmail'].sort(compareFileNamesNumeric), ['email', 'Email', 'Ć©mail', 'Ɖmail'].sort(compareLocale), 'the same base characters with different case or accents sort in locale order'); - - // name plus extensions comparisons - assert(compareFileNamesNumeric('aggregate.go', 'aggregate_repo.go') < 0, 'compares the name first, then the extension'); + assert(compareFileNamesDefault('a', 'A') === compareLocale('a', 'A'), 'the same letter sorts by locale'); + assert(compareFileNamesDefault('Ć¢', 'Ƃ') === compareLocale('Ć¢', 'Ƃ'), 'the same accented letter sorts by locale'); + assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesDefault), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order'); + assert.deepEqual(['email', 'Email', 'Ć©mail', 'Ɖmail'].sort(compareFileNamesDefault), ['email', 'Email', 'Ć©mail', 'Ɖmail'].sort(compareLocale), 'the same base characters with different case or accents sort in locale order'); // numeric comparisons - assert(compareFileNamesNumeric('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first'); - assert(compareFileNamesNumeric('abc.txt1', 'abc.txt01') < 0, 'same name plus extensions with equal numbers sort shortest number first'); - assert(compareFileNamesNumeric('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale'); + assert(compareFileNamesDefault('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first'); + assert(compareFileNamesDefault('abc.txt1', 'abc.txt01') < 0, 'same name plus extensions with equal numbers sort shortest number first'); + assert(compareFileNamesDefault('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale'); }); - test('compareFileExtensionsNumeric', () => { + test('compareFileExtensionsDefault', () => { // // Comparisons with the same result as compareFileExtensions // // name-only comparisons - assert(compareFileExtensionsNumeric(null, null) === 0, 'null should be equal'); - assert(compareFileExtensionsNumeric(null, 'abc') < 0, 'null should come before real files without extensions'); - assert(compareFileExtensionsNumeric('', '') === 0, 'empty should be equal'); - assert(compareFileExtensionsNumeric('abc', 'abc') === 0, 'equal names should be equal'); - assert(compareFileExtensionsNumeric('z', 'A') > 0, 'z comes after A'); - assert(compareFileExtensionsNumeric('Z', 'a') > 0, 'Z comes after a'); + assert(compareFileExtensionsDefault(null, null) === 0, 'null should be equal'); + assert(compareFileExtensionsDefault(null, 'abc') < 0, 'null should come before real files without extensions'); + assert(compareFileExtensionsDefault('', '') === 0, 'empty should be equal'); + assert(compareFileExtensionsDefault('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileExtensionsDefault('z', 'A') > 0, 'z comes after A'); + assert(compareFileExtensionsDefault('Z', 'a') > 0, 'Z comes after a'); // name plus extension comparisons - assert(compareFileExtensionsNumeric('file.ext', 'file.ext') === 0, 'equal full filenames should be equal'); - assert(compareFileExtensionsNumeric('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); - assert(compareFileExtensionsNumeric('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); - assert(compareFileExtensionsNumeric('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first'); - assert(compareFileExtensionsNumeric('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names'); - assert(compareFileExtensionsNumeric('agg.go', 'agg_repo.go') < 0, 'shorter names short before longer names even when the longer name contains an underscore'); - assert(compareFileExtensionsNumeric('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name'); + assert(compareFileExtensionsDefault('file.ext', 'file.ext') === 0, 'equal full filenames should be equal'); + assert(compareFileExtensionsDefault('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileExtensionsDefault('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileExtensionsDefault('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first'); + assert(compareFileExtensionsDefault('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names'); + assert(compareFileExtensionsDefault('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name'); // dotfile comparisons - assert(compareFileExtensionsNumeric('.abc', '.abc') === 0, 'equal dotfiles should be equal'); - assert(compareFileExtensionsNumeric('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); + assert(compareFileExtensionsDefault('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensionsDefault('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); // dotfile vs non-dotfile comparisons - assert(compareFileExtensionsNumeric(null, '.abc') < 0, 'null should come before dotfiles'); - assert(compareFileExtensionsNumeric('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); - assert(compareFileExtensionsNumeric('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + assert(compareFileExtensionsDefault(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileExtensionsDefault('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileExtensionsDefault('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); // numeric comparisons - assert(compareFileExtensionsNumeric('1', '1') === 0, 'numerically equal full names should be equal'); - assert(compareFileExtensionsNumeric('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); - assert(compareFileExtensionsNumeric('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); - assert(compareFileExtensionsNumeric('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order'); - assert(compareFileExtensionsNumeric('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); - assert(compareFileExtensionsNumeric('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); - assert(compareFileExtensionsNumeric('abc2.txt2', 'abc1.txt10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); - assert(compareFileExtensionsNumeric('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal'); - assert(compareFileExtensionsNumeric('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); - assert(compareFileExtensionsNumeric('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long'); - assert(compareFileExtensionsNumeric('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared'); - assert(compareFileExtensionsNumeric('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically'); - - // Same extension comparison that has the same result as compareFileExtensions, but a different result than compareFileNames - // This is an edge case caused by compareFileNames comparing the whole name all at once instead of the name and then the extension. - assert(compareFileExtensionsNumeric('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, names sort in dictionary order'); + assert(compareFileExtensionsDefault('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileExtensionsDefault('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileExtensionsDefault('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsDefault('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order'); + assert(compareFileExtensionsDefault('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileExtensionsDefault('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileExtensionsDefault('abc2.txt2', 'abc1.txt10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsDefault('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal'); + assert(compareFileExtensionsDefault('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsDefault('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileExtensionsDefault('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared'); + assert(compareFileExtensionsDefault('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically'); // // Comparisons with different results than compareFileExtensions // // name-only comparisons - assert(compareFileExtensionsNumeric('a', 'A') === compareLocale('a', 'A'), 'the same letter of different case sorts by locale'); - assert(compareFileExtensionsNumeric('Ć¢', 'Ƃ') === compareLocale('Ć¢', 'Ƃ'), 'the same accented letter of different case sorts by locale'); - assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsNumeric), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order'); - assert.deepEqual(['email', 'Email', 'Ć©mail', 'Ɖmail'].sort(compareFileExtensionsNumeric), ['email', 'Email', 'Ć©mail', 'Ɖmail'].sort((a, b) => a.localeCompare(b)), 'the same base characters with different case or accents sort in locale order'); + assert(compareFileExtensionsDefault('a', 'A') === compareLocale('a', 'A'), 'the same letter of different case sorts by locale'); + assert(compareFileExtensionsDefault('Ć¢', 'Ƃ') === compareLocale('Ć¢', 'Ƃ'), 'the same accented letter of different case sorts by locale'); + assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsDefault), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order'); + assert.deepEqual(['email', 'Email', 'Ć©mail', 'Ɖmail'].sort(compareFileExtensionsDefault), ['email', 'Email', 'Ć©mail', 'Ɖmail'].sort((a, b) => a.localeCompare(b)), 'the same base characters with different case or accents sort in locale order'); // name plus extension comparisons - assert(compareFileExtensionsNumeric('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale'); - assert(compareFileExtensionsNumeric('a.md', 'A.md') === compareLocale('a', 'A'), 'case differences in names sort by locale'); + assert(compareFileExtensionsDefault('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale'); + assert(compareFileExtensionsDefault('a.md', 'A.md') === compareLocale('a', 'A'), 'case differences in names sort by locale'); + assert(compareFileExtensionsDefault('aggregate.go', 'aggregate_repo.go') > 0, 'names with the same extension sort in full filename locale order'); // dotfile comparisons - assert(compareFileExtensionsNumeric('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); - assert(compareFileExtensionsNumeric('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileExtensionsDefault('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileExtensionsDefault('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); // dotfile vs non-dotfile comparisons - assert(compareFileExtensionsNumeric('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); - assert(compareFileExtensionsNumeric('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + assert(compareFileExtensionsDefault('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileExtensionsDefault('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); // numeric comparisons - assert(compareFileExtensionsNumeric('abc.txt01', 'abc.txt1') > 0, 'extensions with equal numbers should be in shortest-first order'); - assert(compareFileExtensionsNumeric('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale'); - assert(compareFileExtensionsNumeric('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first'); - assert(compareFileExtensionsNumeric('txt.abc01', 'txt.abc1') > 0, 'extensions with equivalent numbers sort shortest extension first'); + assert(compareFileExtensionsDefault('abc.txt01', 'abc.txt1') > 0, 'extensions with equal numbers should be in shortest-first order'); + assert(compareFileExtensionsDefault('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale'); + assert(compareFileExtensionsDefault('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first'); + assert(compareFileExtensionsDefault('txt.abc01', 'txt.abc1') > 0, 'extensions with equivalent numbers sort shortest extension first'); }); }); diff --git a/src/vs/base/test/browser/dom.test.ts b/src/vs/base/test/browser/dom.test.ts index 61252159d23..f5c8e65dac6 100644 --- a/src/vs/base/test/browser/dom.test.ts +++ b/src/vs/base/test/browser/dom.test.ts @@ -93,6 +93,22 @@ suite('dom', () => { assert(!div.firstChild); }); + test('should buld nodes with id', () => { + const div = $('div#foo'); + assert(div); + assert(div instanceof HTMLElement); + assert.equal(div.tagName, 'DIV'); + assert.equal(div.id, 'foo'); + }); + + test('should buld nodes with class-name', () => { + const div = $('div.foo'); + assert(div); + assert(div instanceof HTMLElement); + assert.equal(div.tagName, 'DIV'); + assert.equal(div.className, 'foo'); + }); + test('should build nodes with attributes', () => { let div = $('div', { class: 'test' }); assert.equal(div.className, 'test'); @@ -111,5 +127,12 @@ suite('dom', () => { assert.equal(div.firstChild && div.firstChild.textContent, 'hello'); }); + + test('should build nodes with text children', () => { + let div = $('div', undefined, 'foobar'); + let firstChild = div.firstChild as HTMLElement; + assert.equal(firstChild.tagName, undefined); + assert.equal(firstChild.textContent, 'foobar'); + }); }); }); diff --git a/src/vs/base/test/browser/markdownRenderer.test.ts b/src/vs/base/test/browser/markdownRenderer.test.ts index 6ec7827c752..78f82030b2c 100644 --- a/src/vs/base/test/browser/markdownRenderer.test.ts +++ b/src/vs/base/test/browser/markdownRenderer.test.ts @@ -18,7 +18,6 @@ suite('MarkdownRenderer', () => { const result: HTMLElement = renderMarkdown(markdown); const renderer = new marked.Renderer(); const imageFromMarked = marked(markdown.value, { - sanitize: true, renderer }).trim(); assert.strictEqual(result.innerHTML, imageFromMarked); @@ -29,7 +28,6 @@ suite('MarkdownRenderer', () => { const result: HTMLElement = renderMarkdown(markdown); const renderer = new marked.Renderer(); const imageFromMarked = marked(markdown.value, { - sanitize: true, renderer }).trim(); assert.strictEqual(result.innerHTML, imageFromMarked); diff --git a/src/vs/base/test/common/async.test.ts b/src/vs/base/test/common/async.test.ts index 25e7d83cc7b..9d133c91dfa 100644 --- a/src/vs/base/test/common/async.test.ts +++ b/src/vs/base/test/common/async.test.ts @@ -688,4 +688,22 @@ suite('Async', () => { assert.ok(Date.now() - now < 100); assert.equal(timedout, false); }); + + test('SequencerByKey', async () => { + const s = new async.SequencerByKey(); + + const r1 = await s.queue('key1', () => Promise.resolve('hello')); + assert.equal(r1, 'hello'); + + await s.queue('key2', () => Promise.reject(new Error('failed'))).then(() => { + throw new Error('should not be resolved'); + }, err => { + // Expected error + assert.equal(err.message, 'failed'); + }); + + // Still works after a queued promise is rejected + const r3 = await s.queue('key2', () => Promise.resolve('hello')); + assert.equal(r3, 'hello'); + }); }); diff --git a/src/vs/base/test/common/extpath.test.ts b/src/vs/base/test/common/extpath.test.ts index 0760e2c8b91..03993437ffc 100644 --- a/src/vs/base/test/common/extpath.test.ts +++ b/src/vs/base/test/common/extpath.test.ts @@ -57,6 +57,13 @@ suite('Paths', () => { assert.ok(!extpath.isValidBasename('aux')); assert.ok(!extpath.isValidBasename('Aux')); assert.ok(!extpath.isValidBasename('LPT0')); + assert.ok(!extpath.isValidBasename('aux.txt')); + assert.ok(!extpath.isValidBasename('com0.abc')); + assert.ok(extpath.isValidBasename('LPT00')); + assert.ok(extpath.isValidBasename('aux1')); + assert.ok(extpath.isValidBasename('aux1.txt')); + assert.ok(extpath.isValidBasename('aux1.aux.txt')); + assert.ok(!extpath.isValidBasename('test.txt.')); assert.ok(!extpath.isValidBasename('test.txt..')); assert.ok(!extpath.isValidBasename('test.txt ')); diff --git a/src/vs/base/test/common/fuzzyScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts index 569becfed7b..5aac1fb98b4 100644 --- a/src/vs/base/test/common/fuzzyScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -110,10 +110,10 @@ suite('Fuzzy Scorer', () => { scores.push(_doScore(target, 'hw', true)); // direct mix-case prefix (multiple) scores.push(_doScore(target, 'H', true)); // direct case prefix scores.push(_doScore(target, 'h', true)); // direct mix-case prefix - scores.push(_doScore(target, 'ld', true)); // in-string mix-case match (consecutive, avoids scattered hit) scores.push(_doScore(target, 'W', true)); // direct case word prefix - scores.push(_doScore(target, 'w', true)); // direct mix-case word prefix scores.push(_doScore(target, 'Ld', true)); // in-string case match (multiple) + scores.push(_doScore(target, 'ld', true)); // in-string mix-case match (consecutive, avoids scattered hit) + scores.push(_doScore(target, 'w', true)); // direct mix-case word prefix scores.push(_doScore(target, 'L', true)); // in-string case match scores.push(_doScore(target, 'l', true)); // in-string mix-case match scores.push(_doScore(target, '4', true)); // no match @@ -123,13 +123,13 @@ suite('Fuzzy Scorer', () => { assert.deepEqual(scores, sortedScores); // Assert scoring positions - let positions = scores[0][1]; - assert.equal(positions.length, 'HelLo-World'.length); + // let positions = scores[0][1]; + // assert.equal(positions.length, 'HelLo-World'.length); - positions = scores[2][1]; - assert.equal(positions.length, 'HW'.length); - assert.equal(positions[0], 0); - assert.equal(positions[1], 6); + // positions = scores[2][1]; + // assert.equal(positions.length, 'HW'.length); + // assert.equal(positions[0], 0); + // assert.equal(positions[1], 6); }); test('score (non fuzzy)', function () { @@ -626,6 +626,21 @@ suite('Fuzzy Scorer', () => { assert.equal(res[1], resourceA); }); + test('compareFilesByScore - prefer camel case matches', function () { + const resourceA = URI.file('config/test/NullPointerException.java'); + const resourceB = URI.file('config/test/nopointerexception.java'); + + for (const query of ['npe', 'NPE']) { + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + } + }); + test('compareFilesByScore - prefer more compact camel case matches', function () { const resourceA = URI.file('config/test/openthisAnythingHandler.js'); const resourceB = URI.file('config/test/openthisisnotsorelevantforthequeryAnyHand.js'); @@ -925,6 +940,136 @@ suite('Fuzzy Scorer', () => { assert.equal(res[0], resourceB); }); + test('compareFilesByScore - prefer shorter match (bug #103052) - foo bar', function () { + const resourceA = URI.file('app/emails/foo.bar.js'); + const resourceB = URI.file('app/emails/other-footer.other-bar.js'); + + for (const query of ['foo bar', 'foobar']) { + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + } + }); + + test('compareFilesByScore - prefer shorter match (bug #103052) - payment model', function () { + const resourceA = URI.file('app/components/payment/payment.model.js'); + const resourceB = URI.file('app/components/online-payments-history/online-payments-history.model.js'); + + for (const query of ['payment model', 'paymentmodel']) { + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + } + }); + + test('compareFilesByScore - prefer shorter match (bug #103052) - color', function () { + const resourceA = URI.file('app/constants/color.js'); + const resourceB = URI.file('app/components/model/input/pick-avatar-color.js'); + + for (const query of ['color js', 'colorjs']) { + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + } + }); + + test('compareFilesByScore - prefer strict case prefix', function () { + const resourceA = URI.file('app/constants/color.js'); + const resourceB = URI.file('app/components/model/input/Color.js'); + + let query = 'Color'; + + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + assert.equal(res[1], resourceA); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + assert.equal(res[1], resourceA); + + query = 'color'; + + res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + }); + + test('compareFilesByScore - prefer prefix (bug #103052)', function () { + const resourceA = URI.file('test/smoke/src/main.ts'); + const resourceB = URI.file('src/vs/editor/common/services/semantikTokensProviderStyling.ts'); + + let query = 'smoke main.ts'; + + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + }); + + test('compareFilesByScore - boost better prefix match if multiple queries are used', function () { + const resourceA = URI.file('src/vs/workbench/services/host/browser/browserHostService.ts'); + const resourceB = URI.file('src/vs/workbench/browser/workbench.ts'); + + for (const query of ['workbench.ts browser', 'browser workbench.ts', 'browser workbench', 'workbench browser']) { + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + assert.equal(res[1], resourceA); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + assert.equal(res[1], resourceA); + } + }); + + test('compareFilesByScore - boost shorter prefix match if multiple queries are used', function () { + const resourceA = URI.file('src/vs/workbench/browser/actions/windowActions.ts'); + const resourceB = URI.file('src/vs/workbench/electron-browser/window.ts'); + + for (const query of ['window browser', 'window.ts browser']) { + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + assert.equal(res[1], resourceA); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + assert.equal(res[1], resourceA); + } + }); + + test('compareFilesByScore - boost shorter prefix match if multiple queries are used (#99171)', function () { + const resourceA = URI.file('mesh_editor_lifetime_job.h'); + const resourceB = URI.file('lifetime_job.h'); + + for (const query of ['m life, life m']) { + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + assert.equal(res[1], resourceA); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + assert.equal(res[1], resourceA); + } + }); + test('prepareQuery', () => { assert.equal(scorer.prepareQuery(' f*a ').normalized, 'fa'); assert.equal(scorer.prepareQuery('model Tester.ts').original, 'model Tester.ts'); diff --git a/src/vs/base/test/common/hash.test.ts b/src/vs/base/test/common/hash.test.ts index 3225caf7b23..b5074f4ffa5 100644 --- a/src/vs/base/test/common/hash.test.ts +++ b/src/vs/base/test/common/hash.test.ts @@ -32,12 +32,18 @@ suite('Hash', () => { assert.equal(hash([1, 2, 3]), hash([1, 2, 3])); assert.equal(hash(['foo', 'bar']), hash(['foo', 'bar'])); assert.equal(hash([]), hash([])); + assert.equal(hash([]), hash(new Array())); assert.notEqual(hash(['foo', 'bar']), hash(['bar', 'foo'])); assert.notEqual(hash(['foo', 'bar']), hash(['bar', 'foo', null])); + assert.notEqual(hash(['foo', 'bar', null]), hash(['bar', 'foo', null])); + assert.notEqual(hash(['foo', 'bar']), hash(['bar', 'foo', undefined])); + assert.notEqual(hash(['foo', 'bar', undefined]), hash(['bar', 'foo', undefined])); + assert.notEqual(hash(['foo', 'bar', null]), hash(['foo', 'bar', undefined])); }); test('object', () => { assert.equal(hash({}), hash({})); + assert.equal(hash({}), hash(Object.create(null))); assert.equal(hash({ 'foo': 'bar' }), hash({ 'foo': 'bar' })); assert.equal(hash({ 'foo': 'bar', 'foo2': undefined }), hash({ 'foo2': undefined, 'foo': 'bar' })); assert.notEqual(hash({ 'foo': 'bar' }), hash({ 'foo': 'bar2' })); @@ -45,14 +51,26 @@ suite('Hash', () => { }); test('array - unexpected collision', function () { - this.skip(); const a = hash([undefined, undefined, undefined, undefined, undefined]); const b = hash([undefined, undefined, 'HHHHHH', [{ line: 0, character: 0 }, { line: 0, character: 0 }], undefined]); - // console.log(a); - // console.log(b); assert.notEqual(a, b); }); + test('all different', () => { + const candidates: any[] = [ + null, undefined, {}, [], 0, false, true, '', ' ', [null], [undefined], [undefined, undefined], { '': undefined }, { [' ']: undefined }, + 'ab', 'ba', ['ab'] + ]; + const hashes: number[] = candidates.map(hash); + for (let i = 0; i < hashes.length; i++) { + assert.equal(hashes[i], hash(candidates[i])); // verify that repeated invocation returns the same hash + for (let k = i + 1; k < hashes.length; k++) { + assert.notEqual(hashes[i], hashes[k], `Same hash ${hashes[i]} for ${JSON.stringify(candidates[i])} and ${JSON.stringify(candidates[k])}`); + } + } + }); + + function checkSHA1(strings: string[], expected: string) { const hash = new StringSHA1(); for (const str of strings) { diff --git a/src/vs/base/test/common/lifecycle.test.ts b/src/vs/base/test/common/lifecycle.test.ts index 91f17aedb44..7aa87cc6b0f 100644 --- a/src/vs/base/test/common/lifecycle.test.ts +++ b/src/vs/base/test/common/lifecycle.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 { IDisposable, dispose, ReferenceCollection } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, IDisposable, MultiDisposeError, ReferenceCollection, toDisposable } from 'vs/base/common/lifecycle'; class Disposable implements IDisposable { isDisposed = false; @@ -49,6 +49,48 @@ suite('Lifecycle', () => { assert(disposable2.isDisposed); }); + test('dispose array should dispose all if a child throws on dispose', () => { + const disposedValues = new Set(); + + let thrownError: any; + try { + dispose([ + toDisposable(() => { disposedValues.add(1); }), + toDisposable(() => { throw new Error('I am error'); }), + toDisposable(() => { disposedValues.add(3); }), + ]); + } catch (e) { + thrownError = e; + } + + assert.ok(disposedValues.has(1)); + assert.ok(disposedValues.has(3)); + assert.strictEqual(thrownError.message, 'I am error'); + }); + + test('dispose array should rethrow composite error if multiple entries throw on dispose', () => { + const disposedValues = new Set(); + + let thrownError: any; + try { + dispose([ + toDisposable(() => { disposedValues.add(1); }), + toDisposable(() => { throw new Error('I am error 1'); }), + toDisposable(() => { throw new Error('I am error 2'); }), + toDisposable(() => { disposedValues.add(4); }), + ]); + } catch (e) { + thrownError = e; + } + + assert.ok(disposedValues.has(1)); + assert.ok(disposedValues.has(4)); + assert.ok(thrownError instanceof MultiDisposeError); + assert.strictEqual((thrownError as MultiDisposeError).errors.length, 2); + assert.strictEqual((thrownError as MultiDisposeError).errors[0].message, 'I am error 1'); + assert.strictEqual((thrownError as MultiDisposeError).errors[1].message, 'I am error 2'); + }); + test('Action bar has broken accessibility #100273', function () { let array = [{ dispose() { } }, { dispose() { } }]; let array2 = dispose(array); @@ -61,7 +103,52 @@ suite('Lifecycle', () => { let setValues = set.values(); let setValues2 = dispose(setValues); assert.ok(setValues === setValues2); + }); +}); +suite('DisposableStore', () => { + test('dispose should call all child disposes even if a child throws on dispose', () => { + const disposedValues = new Set(); + + const store = new DisposableStore(); + store.add(toDisposable(() => { disposedValues.add(1); })); + store.add(toDisposable(() => { throw new Error('I am error'); })); + store.add(toDisposable(() => { disposedValues.add(3); })); + + let thrownError: any; + try { + store.dispose(); + } catch (e) { + thrownError = e; + } + + assert.ok(disposedValues.has(1)); + assert.ok(disposedValues.has(3)); + assert.strictEqual(thrownError.message, 'I am error'); + }); + + test('dispose should throw composite error if multiple children throw on dispose', () => { + const disposedValues = new Set(); + + const store = new DisposableStore(); + store.add(toDisposable(() => { disposedValues.add(1); })); + store.add(toDisposable(() => { throw new Error('I am error 1'); })); + store.add(toDisposable(() => { throw new Error('I am error 2'); })); + store.add(toDisposable(() => { disposedValues.add(4); })); + + let thrownError: any; + try { + store.dispose(); + } catch (e) { + thrownError = e; + } + + assert.ok(disposedValues.has(1)); + assert.ok(disposedValues.has(4)); + assert.ok(thrownError instanceof MultiDisposeError); + assert.strictEqual((thrownError as MultiDisposeError).errors.length, 2); + assert.strictEqual((thrownError as MultiDisposeError).errors[0].message, 'I am error 1'); + assert.strictEqual((thrownError as MultiDisposeError).errors[1].message, 'I am error 2'); }); }); diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index c82436e3b82..fd324076b26 100644 --- a/src/vs/base/test/node/pfs/pfs.test.ts +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -224,7 +224,6 @@ suite('PFS', function () { } catch (error) { assert.fail(error); - throw error; } }); diff --git a/src/vs/code/browser/workbench/workbench-dev.html b/src/vs/code/browser/workbench/workbench-dev.html index 6196015ad64..aeb8c5e1e66 100644 --- a/src/vs/code/browser/workbench/workbench-dev.html +++ b/src/vs/code/browser/workbench/workbench-dev.html @@ -14,8 +14,11 @@ - + + + + @@ -30,7 +33,6 @@ diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html index 941f2e55399..f9c46333e2e 100644 --- a/src/vs/code/browser/workbench/workbench.html +++ b/src/vs/code/browser/workbench/workbench.html @@ -31,7 +31,6 @@ + + + + + + diff --git a/src/vs/code/electron-sandbox/workbench/workbench.js b/src/vs/code/electron-sandbox/workbench/workbench.js new file mode 100644 index 00000000000..bac5dd6d6e8 --- /dev/null +++ b/src/vs/code/electron-sandbox/workbench/workbench.js @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * 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 perf = (function () { + globalThis.MonacoPerformanceMarks = globalThis.MonacoPerformanceMarks || []; + return { + /** + * @param {string} name + */ + mark(name) { + globalThis.MonacoPerformanceMarks.push(name, Date.now()); + } + }; +})(); + +perf.mark('renderer/started'); + +/** + * @type {{ + * load: (modules: string[], resultCallback: (result, configuration: object) => any, options: object) => unknown, + * globals: () => typeof import('../../../base/parts/sandbox/electron-sandbox/globals') + * }} + */ +const bootstrapWindow = (() => { + // @ts-ignore (defined in bootstrap-window.js) + return window.MonacoBootstrapWindow; +})(); + +// Load environment in parallel to workbench loading to avoid waterfall +const whenEnvResolved = bootstrapWindow.globals().process.whenEnvResolved; + +// Load workbench main JS, CSS and NLS all in parallel. This is an +// optimization to prevent a waterfall of loading to happen, because +// we know for a fact that workbench.desktop.sandbox.main will depend on +// the related CSS and NLS counterparts. +bootstrapWindow.load([ + 'vs/workbench/workbench.desktop.sandbox.main', + 'vs/nls!vs/workbench/workbench.desktop.main', + 'vs/css!vs/workbench/workbench.desktop.main' +], + async function (workbench, configuration) { + + // Mark start of workbench + perf.mark('didLoadWorkbenchMain'); + performance.mark('workbench-start'); + + // Wait for process environment being fully resolved + await whenEnvResolved; + + perf.mark('main/startup'); + + // @ts-ignore + return require('vs/workbench/electron-sandbox/desktop.main').main(configuration); + }, + { + removeDeveloperKeybindingsAfterLoad: true, + canModifyDOM: function (windowConfig) { + // TODO@sandbox part-splash is non-sandboxed only + }, + beforeLoaderConfig: function (windowConfig, loaderConfig) { + loaderConfig.recordStats = true; + }, + beforeRequire: function () { + perf.mark('willLoadWorkbenchMain'); + } + } +); diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 83a9bc61e54..69f2f37b3df 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -14,7 +14,7 @@ import * as paths from 'vs/base/common/path'; import { whenDeleted, writeFileSync } from 'vs/base/node/pfs'; import { findFreePort, randomPort } from 'vs/base/node/ports'; import { isWindows, isLinux } from 'vs/base/common/platform'; -import { ProfilingSession, Target } from 'v8-inspect-profiler'; +import type { ProfilingSession, Target } from 'v8-inspect-profiler'; import { isString } from 'vs/base/common/types'; import { hasStdinWithoutTty, stdinDataListener, getStdinFilePath, readFromStdin } from 'vs/platform/environment/node/stdin'; diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 9b06d39b0bc..b528f7dd343 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -7,7 +7,6 @@ import { localize } from 'vs/nls'; import { raceTimeout } from 'vs/base/common/async'; import product from 'vs/platform/product/common/product'; import * as path from 'vs/base/common/path'; -import * as semver from 'semver-umd'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -214,6 +213,8 @@ export class Main { throw new Error('Invalid vsix'); } + const semver = await import('semver-umd'); + const extensionIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User); const newer = installedExtensions.find(local => areSameExtensions(extensionIdentifier, local.identifier) && semver.gt(local.manifest.version, manifest.version)); diff --git a/src/vs/code/node/shellEnv.ts b/src/vs/code/node/shellEnv.ts index 174bb673a4a..0383550627a 100644 --- a/src/vs/code/node/shellEnv.ts +++ b/src/vs/code/node/shellEnv.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as cp from 'child_process'; +import { spawn } from 'child_process'; import { generateUuid } from 'vs/base/common/uuid'; import { isWindows } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; @@ -30,7 +30,7 @@ function getUnixShellEnvironment(logService: ILogService): Promise ({})); } - -let _shellEnv: Promise; +let shellEnvPromise: Promise | undefined = undefined; /** * We need to get the environment from a user's shell. @@ -91,21 +90,21 @@ let _shellEnv: Promise; * from within a shell. */ export function getShellEnvironment(logService: ILogService, environmentService: INativeEnvironmentService): Promise { - if (_shellEnv === undefined) { + if (!shellEnvPromise) { if (environmentService.args['disable-user-env-probe']) { logService.trace('getShellEnvironment: disable-user-env-probe set, skipping'); - _shellEnv = Promise.resolve({}); + shellEnvPromise = Promise.resolve({}); } else if (isWindows) { logService.trace('getShellEnvironment: running on Windows, skipping'); - _shellEnv = Promise.resolve({}); + shellEnvPromise = Promise.resolve({}); } else if (process.env['VSCODE_CLI'] === '1' && process.env['VSCODE_FORCE_USER_ENV'] !== '1') { logService.trace('getShellEnvironment: running on CLI, skipping'); - _shellEnv = Promise.resolve({}); + shellEnvPromise = Promise.resolve({}); } else { logService.trace('getShellEnvironment: running on Unix'); - _shellEnv = getUnixShellEnvironment(logService); + shellEnvPromise = getUnixShellEnvironment(logService); } } - return _shellEnv; + return shellEnvPromise; } diff --git a/src/vs/code/test/electron-main/nativeHelpers.test.ts b/src/vs/code/test/electron-main/nativeHelpers.test.ts index 199fa7acaf7..1ce46448038 100644 --- a/src/vs/code/test/electron-main/nativeHelpers.test.ts +++ b/src/vs/code/test/electron-main/nativeHelpers.test.ts @@ -28,9 +28,8 @@ suite('Windows Native Helpers', () => { }); test('vscode-windows-ca-certs', async () => { - const windowsCerts = await new Promise((resolve, reject) => { - require(['vscode-windows-ca-certs'], resolve, reject); - }); + // @ts-ignore Windows only + const windowsCerts = await import('vscode-windows-ca-certs'); assert.ok(windowsCerts, 'Unable to load vscode-windows-ca-certs dependency.'); }); diff --git a/src/vs/editor/browser/config/charWidthReader.ts b/src/vs/editor/browser/config/charWidthReader.ts index 479a35c7bea..97fe48dd732 100644 --- a/src/vs/editor/browser/config/charWidthReader.ts +++ b/src/vs/editor/browser/config/charWidthReader.ts @@ -124,12 +124,12 @@ class DomCharWidthReader { private static _render(testElement: HTMLElement, request: CharWidthRequest): void { if (request.chr === ' ') { - let htmlString = ' '; + let htmlString = '\u00a0'; // Repeat character 256 (2^8) times for (let i = 0; i < 8; i++) { htmlString += htmlString; } - testElement.innerHTML = htmlString; + testElement.innerText = htmlString; } else { let testString = request.chr; // Repeat character 256 (2^8) times diff --git a/src/vs/editor/browser/controller/coreCommands.ts b/src/vs/editor/browser/controller/coreCommands.ts index 2013ad47528..63e9488203a 100644 --- a/src/vs/editor/browser/controller/coreCommands.ts +++ b/src/vs/editor/browser/controller/coreCommands.ts @@ -559,9 +559,9 @@ export namespace CoreNavigationCommands { case CursorMove_.Direction.ViewPortCenter: case CursorMove_.Direction.ViewPortIfOutside: return CursorMoveCommands.viewportMove(viewModel, cursors, args.direction, inSelectionMode, value); + default: + return null; } - - return null; } } diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index 9eee0dfe476..220c6c440ea 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -178,14 +178,7 @@ export class TextAreaHandler extends ViewPart { mode }; }, - getScreenReaderContent: (currentState: TextAreaState): TextAreaState => { - - if (browser.isIPad) { - // Do not place anything in the textarea for the iPad - return TextAreaState.EMPTY; - } - if (this._accessibilitySupport === AccessibilitySupport.Disabled) { // We know for a fact that a screen reader is not attached // On OSX, we write the character before the cursor to allow for "long-press" composition diff --git a/src/vs/editor/browser/controller/textAreaInput.ts b/src/vs/editor/browser/controller/textAreaInput.ts index 238e47cd542..7b2868aa701 100644 --- a/src/vs/editor/browser/controller/textAreaInput.ts +++ b/src/vs/editor/browser/controller/textAreaInput.ts @@ -72,7 +72,7 @@ interface InMemoryClipboardMetadata { * Every time we read from the cipboard, if the text matches our last written text, * we can fetch the previous metadata. */ -class InMemoryClipboardMetadataManager { +export class InMemoryClipboardMetadataManager { public static readonly INSTANCE = new InMemoryClipboardMetadataManager(); private _lastState: InMemoryClipboardMetadata | null; diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index b69c69a1c2d..aa1ea51f927 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -348,6 +348,14 @@ export interface IEditorConstructionOptions extends IEditorOptions { overflowWidgetsDomNode?: HTMLElement; } +export interface IDiffEditorConstructionOptions extends IDiffEditorOptions { + /** + * Place overflow widgets inside an external DOM node. + * Defaults to an internal DOM node. + */ + overflowWidgetsDomNode?: HTMLElement; +} + /** * A rich code editor. */ @@ -580,6 +588,11 @@ export interface ICodeEditor extends editorCommon.IEditor { */ getRawOptions(): IEditorOptions; + /** + * @internal + */ + getOverflowWidgetsDomNode(): HTMLElement | undefined; + /** * @internal */ @@ -1055,3 +1068,14 @@ export function getCodeEditor(thing: any): ICodeEditor | null { return null; } + +/** + *@internal + */ +export function getIEditor(thing: any): editorCommon.IEditor | null { + if (isCodeEditor(thing) || isDiffEditor(thing)) { + return thing; + } + + return null; +} diff --git a/src/vs/editor/browser/services/bulkEditService.ts b/src/vs/editor/browser/services/bulkEditService.ts index 37393b426e3..7a1f3346e46 100644 --- a/src/vs/editor/browser/services/bulkEditService.ts +++ b/src/vs/editor/browser/services/bulkEditService.ts @@ -4,13 +4,64 @@ *--------------------------------------------------------------------------------------------*/ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { WorkspaceEdit } from 'vs/editor/common/modes'; +import { TextEdit, WorkspaceEdit, WorkspaceEditMetadata, WorkspaceFileEdit, WorkspaceFileEditOptions, WorkspaceTextEdit } from 'vs/editor/common/modes'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { isObject } from 'vs/base/common/types'; export const IBulkEditService = createDecorator('IWorkspaceEditService'); +function isWorkspaceFileEdit(thing: any): thing is WorkspaceFileEdit { + return isObject(thing) && (Boolean((thing).newUri) || Boolean((thing).oldUri)); +} + +function isWorkspaceTextEdit(thing: any): thing is WorkspaceTextEdit { + return isObject(thing) && URI.isUri((thing).resource) && isObject((thing).edit); +} + +export class ResourceEdit { + + protected constructor(readonly metadata?: WorkspaceEditMetadata) { } + + static convert(edit: WorkspaceEdit): ResourceEdit[] { + + + return edit.edits.map(edit => { + if (isWorkspaceTextEdit(edit)) { + return new ResourceTextEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata); + } + if (isWorkspaceFileEdit(edit)) { + return new ResourceFileEdit(edit.oldUri, edit.newUri, edit.options, edit.metadata); + } + throw new Error('Unsupported edit'); + }); + } +} + +export class ResourceTextEdit extends ResourceEdit { + constructor( + readonly resource: URI, + readonly textEdit: TextEdit, + readonly versionId?: number, + readonly metadata?: WorkspaceEditMetadata + ) { + super(metadata); + } +} + +export class ResourceFileEdit extends ResourceEdit { + constructor( + readonly oldResource: URI | undefined, + readonly newResource: URI | undefined, + readonly options?: WorkspaceFileEditOptions, + readonly metadata?: WorkspaceEditMetadata + ) { + super(metadata); + } +} + export interface IBulkEditOptions { editor?: ICodeEditor; progress?: IProgress; @@ -23,7 +74,7 @@ export interface IBulkEditResult { ariaSummary: string; } -export type IBulkEditPreviewHandler = (edit: WorkspaceEdit, options?: IBulkEditOptions) => Promise; +export type IBulkEditPreviewHandler = (edits: ResourceEdit[], options?: IBulkEditOptions) => Promise; export interface IBulkEditService { readonly _serviceBrand: undefined; @@ -32,6 +83,5 @@ export interface IBulkEditService { setPreviewHandler(handler: IBulkEditPreviewHandler): IDisposable; - apply(edit: WorkspaceEdit, options?: IBulkEditOptions): Promise; + apply(edit: ResourceEdit[], options?: IBulkEditOptions): Promise; } - diff --git a/src/vs/editor/browser/view/viewController.ts b/src/vs/editor/browser/view/viewController.ts index 69880b0133d..519a26bb5f8 100644 --- a/src/vs/editor/browser/view/viewController.ts +++ b/src/vs/editor/browser/view/viewController.ts @@ -109,8 +109,9 @@ export class ViewController { return data.ctrlKey; case 'metaKey': return data.metaKey; + default: + return false; } - return false; } private _hasNonMulticursorModifier(data: IMouseDispatchData): boolean { @@ -121,8 +122,9 @@ export class ViewController { return data.altKey || data.metaKey; case 'metaKey': return data.ctrlKey || data.altKey; + default: + return false; } - return false; } public dispatchMouse(data: IMouseDispatchData): void { diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index 46d36912654..65798b74acc 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -72,7 +72,7 @@ export class DomReadingContext { export class ViewLineOptions { public readonly themeType: ThemeType; - public readonly renderWhitespace: 'none' | 'boundary' | 'selection' | 'all'; + public readonly renderWhitespace: 'none' | 'boundary' | 'selection' | 'trailing' | 'all'; public readonly renderControlCharacters: boolean; public readonly spaceWidth: number; public readonly middotWidth: number; diff --git a/src/vs/editor/browser/viewParts/selections/selections.ts b/src/vs/editor/browser/viewParts/selections/selections.ts index 47a977fe45c..cdb00570ce0 100644 --- a/src/vs/editor/browser/viewParts/selections/selections.ts +++ b/src/vs/editor/browser/viewParts/selections/selections.ts @@ -217,7 +217,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { endStyle.top = CornerStyle.INTERN; } } else if (previousFrameTop) { - // Accept some hick-ups near the viewport edges to save on repaints + // Accept some hiccups near the viewport edges to save on repaints startStyle.top = previousFrameTop.startStyle!.top; endStyle.top = previousFrameTop.endStyle!.top; } @@ -239,7 +239,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { endStyle.bottom = CornerStyle.INTERN; } } else if (previousFrameBottom) { - // Accept some hick-ups near the viewport edges to save on repaints + // Accept some hiccups near the viewport edges to save on repaints startStyle.bottom = previousFrameBottom.startStyle!.bottom; endStyle.bottom = previousFrameBottom.endStyle!.bottom; } diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index aec462f71f0..17539558c91 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -379,6 +379,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return this._configuration.getRawOptions(); } + public getOverflowWidgetsDomNode(): HTMLElement | undefined { + return this._overflowWidgetsDomNode; + } + public getConfiguredWordAtPosition(position: Position): IWordAtPosition | null { if (!this._modelData) { return null; diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 3ad36126dc8..8be83dcae5f 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -70,7 +70,7 @@ interface IEditorsZones { modified: IMyViewZone[]; } -interface IDiffEditorWidgetStyle { +export interface IDiffEditorWidgetStyle { getEditorsDiffDecorations(lineChanges: editorCommon.ILineChange[], ignoreTrimWhitespace: boolean, renderIndicators: boolean, originalWhitespaces: IEditorWhitespace[], modifiedWhitespaces: IEditorWhitespace[], originalEditor: editorBrowser.ICodeEditor, modifiedEditor: editorBrowser.ICodeEditor): IEditorsDiffDecorationsWithZones; setEnableSplitViewResizing(enableSplitViewResizing: boolean): void; applyColors(theme: IColorTheme): boolean; @@ -175,6 +175,9 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE private readonly _onDidUpdateDiff: Emitter = this._register(new Emitter()); public readonly onDidUpdateDiff: Event = this._onDidUpdateDiff.event; + private readonly _onDidContentSizeChange: Emitter = this._register(new Emitter()); + public readonly onDidContentSizeChange: Event = this._onDidContentSizeChange.event; + private readonly id: number; private _state: editorBrowser.DiffEditorState; private _updatingDiffProgress: IProgressRunner | null; @@ -227,7 +230,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE constructor( domElement: HTMLElement, - options: IDiffEditorOptions, + options: editorBrowser.IDiffEditorConstructionOptions, @IClipboardService clipboardService: IClipboardService, @IEditorWorkerService editorWorkerService: IEditorWorkerService, @IContextKeyService contextKeyService: IContextKeyService, @@ -348,21 +351,19 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._diffComputationResult = null; const leftContextKeyService = this._contextKeyService.createScoped(); - leftContextKeyService.createKey('isInDiffLeftEditor', true); const leftServices = new ServiceCollection(); leftServices.set(IContextKeyService, leftContextKeyService); const leftScopedInstantiationService = instantiationService.createChild(leftServices); const rightContextKeyService = this._contextKeyService.createScoped(); - rightContextKeyService.createKey('isInDiffRightEditor', true); const rightServices = new ServiceCollection(); rightServices.set(IContextKeyService, rightContextKeyService); const rightScopedInstantiationService = instantiationService.createChild(rightServices); - this.originalEditor = this._createLeftHandSideEditor(options, leftScopedInstantiationService); - this.modifiedEditor = this._createRightHandSideEditor(options, rightScopedInstantiationService); + this.originalEditor = this._createLeftHandSideEditor(options, leftScopedInstantiationService, leftContextKeyService); + this.modifiedEditor = this._createRightHandSideEditor(options, rightScopedInstantiationService, rightContextKeyService); this._originalOverviewRuler = null; this._modifiedOverviewRuler = null; @@ -372,8 +373,6 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._containerDomElement.appendChild(this._reviewPane.shadow.domNode); this._containerDomElement.appendChild(this._reviewPane.actionBarContainer.domNode); - - // enableSplitViewResizing this._enableSplitViewResizing = true; if (typeof options.enableSplitViewResizing !== 'undefined') { @@ -421,6 +420,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return this._renderIndicators; } + public getContentHeight(): number { + return this.modifiedEditor.getContentHeight(); + } + private _setState(newState: editorBrowser.DiffEditorState): void { if (this._state === newState) { return; @@ -480,7 +483,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._layoutOverviewRulers(); } - private _createLeftHandSideEditor(options: IDiffEditorOptions, instantiationService: IInstantiationService): CodeEditorWidget { + private _createLeftHandSideEditor(options: editorBrowser.IDiffEditorConstructionOptions, instantiationService: IInstantiationService, contextKeyService: IContextKeyService): CodeEditorWidget { const editor = this._createInnerEditor(instantiationService, this._originalDomNode, this._adjustOptionsForLeftHandSide(options, this._originalIsEditable, this._originalCodeLens)); this._register(editor.onDidScrollChange((e) => { @@ -510,10 +513,26 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } })); + const isInDiffLeftEditorKey = contextKeyService.createKey('isInDiffLeftEditor', undefined); + this._register(editor.onDidFocusEditorWidget(() => isInDiffLeftEditorKey.set(true))); + this._register(editor.onDidBlurEditorWidget(() => isInDiffLeftEditorKey.set(false))); + + this._register(editor.onDidContentSizeChange(e => { + const width = this.originalEditor.getContentWidth() + this.modifiedEditor.getContentWidth() + DiffEditorWidget.ONE_OVERVIEW_WIDTH; + const height = Math.max(this.modifiedEditor.getContentHeight(), this.originalEditor.getContentHeight()); + + this._onDidContentSizeChange.fire({ + contentHeight: height, + contentWidth: width, + contentHeightChanged: e.contentHeightChanged, + contentWidthChanged: e.contentWidthChanged + }); + })); + return editor; } - private _createRightHandSideEditor(options: IDiffEditorOptions, instantiationService: IInstantiationService): CodeEditorWidget { + private _createRightHandSideEditor(options: editorBrowser.IDiffEditorConstructionOptions, instantiationService: IInstantiationService, contextKeyService: IContextKeyService): CodeEditorWidget { const editor = this._createInnerEditor(instantiationService, this._modifiedDomNode, this._adjustOptionsForRightHandSide(options, this._modifiedCodeLens)); this._register(editor.onDidScrollChange((e) => { @@ -555,6 +574,22 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } })); + const isInDiffRightEditorKey = contextKeyService.createKey('isInDiffRightEditor', undefined); + this._register(editor.onDidFocusEditorWidget(() => isInDiffRightEditorKey.set(true))); + this._register(editor.onDidBlurEditorWidget(() => isInDiffRightEditorKey.set(false))); + + this._register(editor.onDidContentSizeChange(e => { + const width = this.originalEditor.getContentWidth() + this.modifiedEditor.getContentWidth() + DiffEditorWidget.ONE_OVERVIEW_WIDTH; + const height = Math.max(this.modifiedEditor.getContentHeight(), this.originalEditor.getContentHeight()); + + this._onDidContentSizeChange.fire({ + contentHeight: height, + contentWidth: width, + contentHeightChanged: e.contentHeightChanged, + contentWidthChanged: e.contentWidthChanged + }); + })); + return editor; } @@ -1058,8 +1093,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } } - private _adjustOptionsForSubEditor(options: IDiffEditorOptions): IDiffEditorOptions { - let clonedOptions: IDiffEditorOptions = objects.deepClone(options || {}); + private _adjustOptionsForSubEditor(options: editorBrowser.IDiffEditorConstructionOptions): editorBrowser.IDiffEditorConstructionOptions { + let clonedOptions: editorBrowser.IDiffEditorConstructionOptions = objects.deepClone(options || {}); clonedOptions.inDiffEditor = true; clonedOptions.wordWrap = 'off'; clonedOptions.wordWrapMinified = false; @@ -1069,6 +1104,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE clonedOptions.folding = false; clonedOptions.codeLens = false; clonedOptions.fixedOverflowWidgets = true; + clonedOptions.overflowWidgetsDomNode = options.overflowWidgetsDomNode; // clonedOptions.lineDecorationsWidth = '2ch'; if (!clonedOptions.minimap) { clonedOptions.minimap = {}; @@ -1077,7 +1113,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return clonedOptions; } - private _adjustOptionsForLeftHandSide(options: IDiffEditorOptions, isEditable: boolean, isCodeLensEnabled: boolean): IEditorOptions { + private _adjustOptionsForLeftHandSide(options: editorBrowser.IDiffEditorConstructionOptions, isEditable: boolean, isCodeLensEnabled: boolean): editorBrowser.IEditorConstructionOptions { let result = this._adjustOptionsForSubEditor(options); if (isCodeLensEnabled) { result.codeLens = true; @@ -1087,7 +1123,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return result; } - private _adjustOptionsForRightHandSide(options: IDiffEditorOptions, isCodeLensEnabled: boolean): IEditorOptions { + private _adjustOptionsForRightHandSide(options: editorBrowser.IDiffEditorConstructionOptions, isCodeLensEnabled: boolean): editorBrowser.IEditorConstructionOptions { let result = this._adjustOptionsForSubEditor(options); if (isCodeLensEnabled) { result.codeLens = true; @@ -1610,14 +1646,14 @@ abstract class ViewZonesComputer { protected abstract _produceModifiedFromDiff(lineChange: editorCommon.ILineChange, lineChangeOriginalLength: number, lineChangeModifiedLength: number): IMyViewZone | null; } -function createDecoration(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, options: ModelDecorationOptions) { +export function createDecoration(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, options: ModelDecorationOptions) { return { range: new Range(startLineNumber, startColumn, endLineNumber, endColumn), options: options }; } -const DECORATIONS = { +export const DECORATIONS = { charDelete: ModelDecorationOptions.register({ className: 'char-delete' @@ -1665,7 +1701,7 @@ const DECORATIONS = { }; -class DiffEditorWidgetSideBySide extends DiffEditorWidgetStyle implements IDiffEditorWidgetStyle, IVerticalSashLayoutProvider { +export class DiffEditorWidgetSideBySide extends DiffEditorWidgetStyle implements IDiffEditorWidgetStyle, IVerticalSashLayoutProvider { static readonly MINIMUM_EDITOR_WIDTH = 100; @@ -2193,11 +2229,11 @@ class InlineViewZonesComputer extends ViewZonesComputer { } } -function isChangeOrInsert(lineChange: editorCommon.IChange): boolean { +export function isChangeOrInsert(lineChange: editorCommon.IChange): boolean { return lineChange.modifiedEndLineNumber > 0; } -function isChangeOrDelete(lineChange: editorCommon.IChange): boolean { +export function isChangeOrDelete(lineChange: editorCommon.IChange): boolean { return lineChange.originalEndLineNumber > 0; } diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index 3afb89e70a6..e6e1c34d987 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -702,7 +702,7 @@ export class DiffReview extends Disposable { if (originalLine !== 0) { originalLineNumber.appendChild(document.createTextNode(String(originalLine))); } else { - originalLineNumber.innerHTML = ' '; + originalLineNumber.innerText = '\u00a0'; } cell.appendChild(originalLineNumber); @@ -714,7 +714,7 @@ export class DiffReview extends Disposable { if (modifiedLine !== 0) { modifiedLineNumber.appendChild(document.createTextNode(String(modifiedLine))); } else { - modifiedLineNumber.innerHTML = ' '; + modifiedLineNumber.innerText = '\u00a0'; } cell.appendChild(modifiedLineNumber); @@ -724,10 +724,10 @@ export class DiffReview extends Disposable { if (spacerIcon) { const spacerCodicon = document.createElement('span'); spacerCodicon.className = spacerIcon.classNames; - spacerCodicon.innerHTML = '  '; + spacerCodicon.innerText = '\u00a0\u00a0'; spacer.appendChild(spacerCodicon); } else { - spacer.innerHTML = '  '; + spacer.innerText = '\u00a0\u00a0'; } cell.appendChild(spacer); diff --git a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts index 09a18fcf097..5dd98c444c7 100644 --- a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts +++ b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts @@ -37,7 +37,7 @@ export class EmbeddedCodeEditorWidget extends CodeEditorWidget { @INotificationService notificationService: INotificationService, @IAccessibilityService accessibilityService: IAccessibilityService ) { - super(domElement, parentEditor.getRawOptions(), {}, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService); + super(domElement, { ...parentEditor.getRawOptions(), overflowWidgetsDomNode: parentEditor.getOverflowWidgetsDomNode() }, {}, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService); this._parentEditor = parentEditor; this._overwriteOptions = options; diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 6176dcd8fa0..cc0306c275d 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -529,7 +529,7 @@ export interface IEditorOptions { * Enable rendering of whitespace. * Defaults to none. */ - renderWhitespace?: 'none' | 'boundary' | 'selection' | 'all'; + renderWhitespace?: 'none' | 'boundary' | 'selection' | 'trailing' | 'all'; /** * Enable rendering of control characters. * Defaults to false. @@ -1282,6 +1282,10 @@ class EditorEmptySelectionClipboard extends EditorBooleanOption constructor() { const defaults: EditorFindOptions = { + cursorMoveOnType: true, seedSearchStringFromSelection: true, autoFindInSelection: 'never', globalFindClipboard: false, @@ -1320,6 +1325,11 @@ class EditorFind extends BaseEditorOption super( EditorOption.find, 'find', defaults, { + 'editor.find.cursorMoveOnType': { + type: 'boolean', + default: defaults.cursorMoveOnType, + description: nls.localize('find.cursorMoveOnType', "Controls whether the cursor should jump to find matches while typing.") + }, 'editor.find.seedSearchStringFromSelection': { type: 'boolean', default: defaults.seedSearchStringFromSelection, @@ -1363,6 +1373,7 @@ class EditorFind extends BaseEditorOption } const input = _input as IEditorFindOptions; return { + cursorMoveOnType: EditorBooleanOption.boolean(input.cursorMoveOnType, this.defaultValue.cursorMoveOnType), seedSearchStringFromSelection: EditorBooleanOption.boolean(input.seedSearchStringFromSelection, this.defaultValue.seedSearchStringFromSelection), autoFindInSelection: typeof _input.autoFindInSelection === 'boolean' ? (_input.autoFindInSelection ? 'always' : 'never') @@ -1476,6 +1487,43 @@ class EditorFontSize extends SimpleEditorOption { //#endregion +//#region fontWeight + +class EditorFontWeight extends BaseEditorOption { + private static ENUM_VALUES = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']; + private static MINIMUM_VALUE = 1; + private static MAXIMUM_VALUE = 1000; + + constructor() { + super( + EditorOption.fontWeight, 'fontWeight', EDITOR_FONT_DEFAULTS.fontWeight, + { + anyOf: [ + { + type: 'number', + minimum: EditorFontWeight.MINIMUM_VALUE, + maximum: EditorFontWeight.MAXIMUM_VALUE + }, + { + enum: EditorFontWeight.ENUM_VALUES + } + ], + default: EDITOR_FONT_DEFAULTS.fontWeight, + description: nls.localize('fontWeight', "Controls the font weight.") + } + ); + } + + public validate(input: any): string { + if (typeof input === 'number') { + return EditorFontWeight.MINIMUM_VALUE <= input && input <= EditorFontWeight.MAXIMUM_VALUE ? String(input) : EDITOR_FONT_DEFAULTS.fontWeight; + } + return EditorStringEnumOption.stringSet(input, EDITOR_FONT_DEFAULTS.fontWeight, EditorFontWeight.ENUM_VALUES); + } +} + +//#endregion + //#region gotoLocation export type GoToLocationValues = 'peek' | 'gotoAndPeek' | 'goto'; @@ -1809,7 +1857,7 @@ export interface EditorLayoutInfoComputerEnv { readonly memory: ComputeOptionsMemory | null; readonly outerWidth: number; readonly outerHeight: number; - readonly isDominatedByLongLines: boolean + readonly isDominatedByLongLines: boolean; readonly lineHeight: number; readonly viewLineCount: number; readonly lineNumbersDigitCount: number; @@ -1824,7 +1872,7 @@ export interface EditorLayoutInfoComputerEnv { export interface IEditorLayoutComputerInput { readonly outerWidth: number; readonly outerHeight: number; - readonly isDominatedByLongLines: boolean + readonly isDominatedByLongLines: boolean; readonly lineHeight: number; readonly lineNumbersDigitCount: number; readonly typicalHalfwidthCharacterWidth: number; @@ -3087,7 +3135,7 @@ export interface ISuggestOptions { * Controls the visibility of the status bar at the bottom of the suggest widget. */ visible?: boolean; - } + }; } export type InternalSuggestOptions = Readonly>; @@ -3852,13 +3900,7 @@ export const EditorOptions = { fontInfo: register(new EditorFontInfo()), fontLigatures2: register(new EditorFontLigatures()), fontSize: register(new EditorFontSize()), - fontWeight: register(new EditorStringOption( - EditorOption.fontWeight, 'fontWeight', EDITOR_FONT_DEFAULTS.fontWeight, - { - enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], - description: nls.localize('fontWeight', "Controls the font weight.") - } - )), + fontWeight: register(new EditorFontWeight()), formatOnPaste: register(new EditorBooleanOption( EditorOption.formatOnPaste, 'formatOnPaste', false, { description: nls.localize('formatOnPaste', "Controls whether the editor should automatically format the pasted content. A formatter must be available and the formatter should be able to format a range in a document.") } @@ -4037,13 +4079,14 @@ export const EditorOptions = { )), renderWhitespace: register(new EditorStringEnumOption( EditorOption.renderWhitespace, 'renderWhitespace', - 'selection' as 'selection' | 'none' | 'boundary' | 'all', - ['none', 'boundary', 'selection', 'all'] as const, + 'selection' as 'selection' | 'none' | 'boundary' | 'trailing' | 'all', + ['none', 'boundary', 'selection', 'trailing', 'all'] as const, { enumDescriptions: [ '', nls.localize('renderWhitespace.boundary', "Render whitespace characters except for single spaces between words."), nls.localize('renderWhitespace.selection', "Render whitespace characters only on selected text."), + nls.localize('renderWhitespace.trailing', "Render only trailing whitespace characters"), '' ], description: nls.localize('renderWhitespace', "Controls how the editor should render whitespace characters.") diff --git a/src/vs/editor/common/controller/cursorMoveCommands.ts b/src/vs/editor/common/controller/cursorMoveCommands.ts index 68d4e69ebf0..bc9bbb1c390 100644 --- a/src/vs/editor/common/controller/cursorMoveCommands.ts +++ b/src/vs/editor/common/controller/cursorMoveCommands.ts @@ -317,9 +317,10 @@ export class CursorMoveCommands { // Move to the last non-whitespace column of the current view line return this._moveToViewLastNonWhitespaceColumn(viewModel, cursors, inSelectionMode); } + default: + return null; } - return null; } public static viewportMove(viewModel: IViewModel, cursors: CursorState[], direction: CursorMove.ViewportDirection, inSelectionMode: boolean, value: number): PartialCursorState[] | null { @@ -353,9 +354,9 @@ export class CursorMoveCommands { } return result; } + default: + return null; } - - return null; } public static findPositionInViewportIfOutside(viewModel: IViewModel, cursor: CursorState, visibleViewRange: Range, inSelectionMode: boolean): PartialCursorState { diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 56bb9bab50c..d15e79eab7d 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -800,7 +800,7 @@ export interface ITextModel { /** * Search the model. * @param searchString The string used to search. If it is a regular expression, set `isRegex` to true. - * @param searchScope Limit the searching to only search inside this range. + * @param searchScope Limit the searching to only search inside these ranges. * @param isRegex Used to indicate that `searchString` is a regular expression. * @param matchCase Force the matching to match lower/upper case exactly. * @param wordSeparators Force the matching to match entire words only. Pass null otherwise. @@ -808,7 +808,7 @@ export interface ITextModel { * @param limitResultCount Limit the number of results * @return The ranges where the matches are. It is empty if no matches have been found. */ - findMatches(searchString: string, searchScope: IRange, isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean, limitResultCount?: number): FindMatch[]; + findMatches(searchString: string, searchScope: IRange | IRange[], isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean, limitResultCount?: number): FindMatch[]; /** * Search the model for the next match. Loops to the beginning of the model if needed. * @param searchString The string used to search. If it is a regular expression, set `isRegex` to true. @@ -1312,6 +1312,7 @@ export interface IReadonlyTextBuffer { getLinesContent(): string[]; getLineContent(lineNumber: number): string; getLineCharCode(lineNumber: number, index: number): number; + getCharCode(offset: number): number; getLineLength(lineNumber: number): number; getLineFirstNonWhitespaceColumn(lineNumber: number): number; getLineLastNonWhitespaceColumn(lineNumber: number): number; diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts index e0c7e80cfa6..03a518eb523 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts @@ -626,8 +626,7 @@ export class PieceTreeBase { return this._lastVisitedLine.value; } - public getLineCharCode(lineNumber: number, index: number): number { - let nodePos = this.nodeAt2(lineNumber, index + 1); + private _getCharCode(nodePos: NodePosition): number { if (nodePos.remainder === nodePos.node.piece.length) { // the char we want to fetch is at the head of next node. let matchingNode = nodePos.node.next(); @@ -647,6 +646,11 @@ export class PieceTreeBase { } } + public getLineCharCode(lineNumber: number, index: number): number { + let nodePos = this.nodeAt2(lineNumber, index + 1); + return this._getCharCode(nodePos); + } + public getLineLength(lineNumber: number): number { if (lineNumber === this.getLineCount()) { let startOffset = this.getOffsetAt(lineNumber, 1); @@ -655,6 +659,11 @@ export class PieceTreeBase { return this.getOffsetAt(lineNumber + 1, 1) - this.getOffsetAt(lineNumber, 1) - this._EOLLength; } + public getCharCode(offset: number): number { + let nodePos = this.nodeAt(offset); + return this._getCharCode(nodePos); + } + public findMatchesInNode(node: TreeNode, searcher: Searcher, startLineNumber: number, startColumn: number, startCursor: BufferCursor, endCursor: BufferCursor, searchData: SearchData, captureMatches: boolean, limitResultCount: number, resultLen: number, result: FindMatch[]) { let buffer = this._buffers[node.piece.bufferIndex]; let 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 1c81c18a0a6..62ab2929910 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts @@ -178,6 +178,10 @@ export class PieceTreeTextBuffer implements ITextBuffer, IDisposable { return this._pieceTree.getLineCharCode(lineNumber, index); } + public getCharCode(offset: number): number { + return this._pieceTree.getCharCode(offset); + } + public getLineLength(lineNumber: number): number { return this._pieceTree.getLineLength(lineNumber); } @@ -214,8 +218,9 @@ export class PieceTreeTextBuffer implements ITextBuffer, IDisposable { return '\r\n'; case EndOfLinePreference.TextDefined: return this.getEOL(); + default: + throw new Error('Unknown EOL preference'); } - throw new Error('Unknown EOL preference'); } public setEOL(newEOL: '\r\n' | '\n'): void { diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 4e542299323..cb7b34b3451 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -1121,13 +1121,35 @@ export class TextModel extends Disposable implements model.ITextModel { public findMatches(searchString: string, rawSearchScope: any, isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean, limitResultCount: number = LIMIT_FIND_COUNT): model.FindMatch[] { this._assertNotDisposed(); - let searchRange: Range; - if (Range.isIRange(rawSearchScope)) { - searchRange = this.validateRange(rawSearchScope); - } else { - searchRange = this.getFullModelRange(); + let searchRanges: Range[] | null = null; + + if (rawSearchScope !== null) { + if (!Array.isArray(rawSearchScope)) { + rawSearchScope = [rawSearchScope]; + } + + if (rawSearchScope.every((searchScope: Range) => Range.isIRange(searchScope))) { + searchRanges = rawSearchScope.map((searchScope: Range) => this.validateRange(searchScope)); + } } + if (searchRanges === null) { + searchRanges = [this.getFullModelRange()]; + } + + searchRanges = searchRanges.sort((d1, d2) => d1.startLineNumber - d2.startLineNumber || d1.startColumn - d2.startColumn); + + const uniqueSearchRanges: Range[] = []; + uniqueSearchRanges.push(searchRanges.reduce((prev, curr) => { + if (Range.areIntersecting(prev, curr)) { + return prev.plusRange(curr); + } + + uniqueSearchRanges.push(prev); + return curr; + })); + + let matchMapper: (value: Range, index: number, array: Range[]) => model.FindMatch[]; if (!isRegex && searchString.indexOf('\n') < 0) { // not regex, not multi line const searchParams = new SearchParams(searchString, isRegex, matchCase, wordSeparators); @@ -1137,10 +1159,12 @@ export class TextModel extends Disposable implements model.ITextModel { return []; } - return this.findMatchesLineByLine(searchRange, searchData, captureMatches, limitResultCount); + matchMapper = (searchRange: Range) => this.findMatchesLineByLine(searchRange, searchData, captureMatches, limitResultCount); + } else { + matchMapper = (searchRange: Range) => TextModelSearch.findMatches(this, new SearchParams(searchString, isRegex, matchCase, wordSeparators), searchRange, captureMatches, limitResultCount); } - return TextModelSearch.findMatches(this, new SearchParams(searchString, isRegex, matchCase, wordSeparators), searchRange, captureMatches, limitResultCount); + return uniqueSearchRanges.map(matchMapper).reduce((arr, matches: model.FindMatch[]) => arr.concat(matches), []); } public findNextMatch(searchString: string, rawSearchStart: IPosition, isRegex: boolean, matchCase: boolean, wordSeparators: string, captureMatches: boolean): model.FindMatch | null { diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index b89285449aa..3d34f627b1c 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -8,7 +8,6 @@ import { Color } from 'vs/base/common/color'; import { Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { isObject } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; @@ -813,12 +812,12 @@ export interface DocumentHighlightProvider { */ export interface OnTypeRenameProvider { - stopPattern?: RegExp; + wordPattern?: RegExp; /** * Provide a list of ranges that can be live-renamed together. */ - provideOnTypeRenameRanges(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult; + provideOnTypeRenameRanges(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult<{ ranges: IRange[]; wordPattern?: RegExp; }>; } /** @@ -1337,29 +1336,6 @@ export class FoldingRangeKind { } } -/** - * @internal - */ -export namespace WorkspaceFileEdit { - /** - * @internal - */ - export function is(thing: any): thing is WorkspaceFileEdit { - return isObject(thing) && (Boolean((thing).newUri) || Boolean((thing).oldUri)); - } -} - -/** - * @internal - */ -export namespace WorkspaceTextEdit { - /** - * @internal - */ - export function is(thing: any): thing is WorkspaceTextEdit { - return isObject(thing) && URI.isUri((thing).resource) && isObject((thing).edit); - } -} export interface WorkspaceEditMetadata { needsConfirmation: boolean; diff --git a/src/vs/editor/common/modes/supports/tokenization.ts b/src/vs/editor/common/modes/supports/tokenization.ts index 12566c3b00e..e701b60f0a9 100644 --- a/src/vs/editor/common/modes/supports/tokenization.ts +++ b/src/vs/editor/common/modes/supports/tokenization.ts @@ -399,10 +399,10 @@ export function generateTokensCSSForColorMap(colorMap: Color[]): string { let rules: string[] = []; for (let i = 1, len = colorMap.length; i < len; i++) { let color = colorMap[i]; - rules[i] = `.mtk${i} { color: ${color}; }`; + rules[i] = `.monaco-editor .mtk${i} { color: ${color}; }`; } - rules.push('.mtki { font-style: italic; }'); - rules.push('.mtkb { font-weight: bold; }'); - rules.push('.mtku { text-decoration: underline; text-underline-position: under; }'); + rules.push('.monaco-editor .mtki { font-style: italic; }'); + rules.push('.monaco-editor .mtkb { font-weight: bold; }'); + rules.push('.monaco-editor .mtku { text-decoration: underline; text-underline-position: under; }'); return rules.join('\n'); } diff --git a/src/vs/editor/common/modes/tokenizationRegistry.ts b/src/vs/editor/common/modes/tokenizationRegistry.ts index 3cb7a15ea65..c92c1296b19 100644 --- a/src/vs/editor/common/modes/tokenizationRegistry.ts +++ b/src/vs/editor/common/modes/tokenizationRegistry.ts @@ -7,7 +7,6 @@ import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ColorId, ITokenizationRegistry, ITokenizationSupport, ITokenizationSupportChangedEvent } from 'vs/editor/common/modes'; -import { toArray } from 'vs/base/common/arrays'; export class TokenizationRegistryImpl implements ITokenizationRegistry { @@ -82,7 +81,7 @@ export class TokenizationRegistryImpl implements ITokenizationRegistry { public setColorMap(colorMap: Color[]): void { this._colorMap = colorMap; this._onDidChange.fire({ - changedLanguages: toArray(this._map.keys()), + changedLanguages: Array.from(this._map.keys()), changedColorMap: true }); } diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index fbdc31d6d5c..b20a0b7c58b 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -54,6 +54,11 @@ export function getIconClasses(modelService: IModelService, modeService: IModeSe return classes; } + +export function getIconClassesForModeId(modeId: string): string[] { + return ['file-icon', `${cssEscape(modeId)}-lang-file-icon`]; +} + export function detectModeId(modelService: IModelService, modeService: IModeService, resource: uri): string | null { if (!resource) { return null; // we need a resource at least diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index 3ff97b83674..23640e0af3b 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -14,7 +14,8 @@ export const enum RenderWhitespace { None = 0, Boundary = 1, Selection = 2, - All = 3 + Trailing = 3, + All = 4 } export const enum LinePartMetadata { @@ -113,7 +114,7 @@ export class RenderLineInput { middotWidth: number, wsmiddotWidth: number, stopRenderingLineAfter: number, - renderWhitespace: 'none' | 'boundary' | 'selection' | 'all', + renderWhitespace: 'none' | 'boundary' | 'selection' | 'trailing' | 'all', renderControlCharacters: boolean, fontLigatures: boolean, selectionsOnLine: LineRange[] | null @@ -138,7 +139,9 @@ export class RenderLineInput { ? RenderWhitespace.Boundary : renderWhitespace === 'selection' ? RenderWhitespace.Selection - : RenderWhitespace.None + : renderWhitespace === 'trailing' + ? RenderWhitespace.Trailing + : RenderWhitespace.None ); this.renderControlCharacters = renderControlCharacters; this.fontLigatures = fontLigatures; @@ -435,7 +438,11 @@ function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput } let tokens = transformAndRemoveOverflowing(input.lineTokens, input.fauxIndentLength, len); - if (input.renderWhitespace === RenderWhitespace.All || input.renderWhitespace === RenderWhitespace.Boundary || (input.renderWhitespace === RenderWhitespace.Selection && !!input.selectionsOnLine)) { + if (input.renderWhitespace === RenderWhitespace.All || + input.renderWhitespace === RenderWhitespace.Boundary || + (input.renderWhitespace === RenderWhitespace.Selection && !!input.selectionsOnLine) || + input.renderWhitespace === RenderWhitespace.Trailing) { + tokens = _applyRenderWhitespace(input, lineContent, len, tokens); } let containsForeignElements = ForeignElementType.None; @@ -592,6 +599,7 @@ function _applyRenderWhitespace(input: RenderLineInput, lineContent: string, len const useMonospaceOptimizations = input.useMonospaceOptimizations; const selections = input.selectionsOnLine; const onlyBoundary = (input.renderWhitespace === RenderWhitespace.Boundary); + const onlyTrailing = (input.renderWhitespace === RenderWhitespace.Trailing); const generateLinePartForEachWhitespace = (input.renderSpaceWidth !== input.spaceWidth); let result: LinePart[] = [], resultLen = 0; @@ -600,10 +608,11 @@ function _applyRenderWhitespace(input: RenderLineInput, lineContent: string, len let tokenEndIndex = tokens[tokenIndex].endIndex; const tokensLength = tokens.length; + let lineIsEmptyOrWhitespace = false; let firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent); let lastNonWhitespaceIndex: number; if (firstNonWhitespaceIndex === -1) { - // The entire line is whitespace + lineIsEmptyOrWhitespace = true; firstNonWhitespaceIndex = len; lastNonWhitespaceIndex = len; } else { @@ -651,6 +660,11 @@ function _applyRenderWhitespace(input: RenderLineInput, lineContent: string, len isInWhitespace = !!currentSelection && currentSelection.startOffset <= charIndex && currentSelection.endOffset > charIndex; } + // If rendering only trailing whitespace, check that the charIndex points to trailing whitespace. + if (isInWhitespace && onlyTrailing) { + isInWhitespace = lineIsEmptyOrWhitespace || charIndex > lastNonWhitespaceIndex; + } + if (wasInWhitespace) { // was in whitespace token if (!isInWhitespace || (!useMonospaceOptimizations && tmpIndent >= tabSize)) { diff --git a/src/vs/editor/contrib/clipboard/clipboard.ts b/src/vs/editor/contrib/clipboard/clipboard.ts index 114004565c5..6860a7bb610 100644 --- a/src/vs/editor/contrib/clipboard/clipboard.ts +++ b/src/vs/editor/contrib/clipboard/clipboard.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import * as browser from 'vs/base/browser/browser'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import * as platform from 'vs/base/common/platform'; -import { CopyOptions } from 'vs/editor/browser/controller/textAreaInput'; +import { CopyOptions, InMemoryClipboardMetadataManager } from 'vs/editor/browser/controller/textAreaInput'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, registerEditorAction, Command, MultiCommand } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; @@ -16,6 +16,8 @@ import { MenuId } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { Handler } from 'vs/editor/common/editorCommon'; const CLIPBOARD_CONTEXT_MENU_GROUP = '9_cutcopypaste'; @@ -23,10 +25,9 @@ const supportsCut = (platform.isNative || document.queryCommandSupported('cut')) const supportsCopy = (platform.isNative || document.queryCommandSupported('copy')); // IE and Edge have trouble with setting html content in clipboard const supportsCopyWithSyntaxHighlighting = (supportsCopy && !browser.isEdge); -// Chrome incorrectly returns true for document.queryCommandSupported('paste') -// when the paste feature is available but the calling script has insufficient -// privileges to actually perform the action -const supportsPaste = (platform.isNative || (!browser.isChrome && document.queryCommandSupported('paste'))); +// Firefox only supports navigator.clipboard.readText() in browser extensions. +// See https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText#Browser_compatibility +const supportsPaste = (browser.isFirefox ? document.queryCommandSupported('paste') : true); function registerCommand(command: T): T { command.register(); @@ -160,7 +161,7 @@ class ExecCommandCopyWithSyntaxHighlightingAction extends EditorAction { } } -function registerExecCommandImpl(target: MultiCommand | undefined, browserCommand: 'cut' | 'copy' | 'paste'): void { +function registerExecCommandImpl(target: MultiCommand | undefined, browserCommand: 'cut' | 'copy'): void { if (!target) { return; } @@ -170,13 +171,11 @@ function registerExecCommandImpl(target: MultiCommand | undefined, browserComman // Only if editor text focus (i.e. not if editor has widget focus). const focusedEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor(); if (focusedEditor && focusedEditor.hasTextFocus()) { - if (browserCommand === 'cut' || browserCommand === 'copy') { - // Do not execute if there is no selection and empty selection clipboard is off - const emptySelectionClipboard = focusedEditor.getOption(EditorOption.emptySelectionClipboard); - const selection = focusedEditor.getSelection(); - if (selection && selection.isEmpty() && !emptySelectionClipboard) { - return true; - } + // Do not execute if there is no selection and empty selection clipboard is off + const emptySelectionClipboard = focusedEditor.getOption(EditorOption.emptySelectionClipboard); + const selection = focusedEditor.getSelection(); + if (selection && selection.isEmpty() && !emptySelectionClipboard) { + return true; } document.execCommand(browserCommand); return true; @@ -186,7 +185,6 @@ function registerExecCommandImpl(target: MultiCommand | undefined, browserComman // 2. (default) handle case when focus is somewhere else. target.addImplementation(0, (accessor: ServicesAccessor, args: any) => { - // Only if editor text focus (i.e. not if editor has widget focus). document.execCommand(browserCommand); return true; }); @@ -194,7 +192,52 @@ function registerExecCommandImpl(target: MultiCommand | undefined, browserComman registerExecCommandImpl(CutAction, 'cut'); registerExecCommandImpl(CopyAction, 'copy'); -registerExecCommandImpl(PasteAction, 'paste'); + +if (PasteAction) { + // 1. Paste: handle case when focus is in editor. + PasteAction.addImplementation(10000, (accessor: ServicesAccessor, args: any) => { + const codeEditorService = accessor.get(ICodeEditorService); + const clipboardService = accessor.get(IClipboardService); + + // Only if editor text focus (i.e. not if editor has widget focus). + const focusedEditor = codeEditorService.getFocusedCodeEditor(); + if (focusedEditor && focusedEditor.hasTextFocus()) { + const result = document.execCommand('paste'); + // Use the clipboard service if document.execCommand('paste') was not successful + if (!result && platform.isWeb) { + (async () => { + const clipboardText = await clipboardService.readText(); + if (clipboardText !== '') { + const metadata = InMemoryClipboardMetadataManager.INSTANCE.get(clipboardText); + let pasteOnNewLine = false; + let multicursorText: string[] | null = null; + let mode: string | null = null; + if (metadata) { + pasteOnNewLine = (focusedEditor.getOption(EditorOption.emptySelectionClipboard) && !!metadata.isFromEmptySelection); + multicursorText = (typeof metadata.multicursorText !== 'undefined' ? metadata.multicursorText : null); + mode = metadata.mode; + } + focusedEditor.trigger('keyboard', Handler.Paste, { + text: clipboardText, + pasteOnNewLine, + multicursorText, + mode + }); + } + })(); + return true; + } + return true; + } + return false; + }); + + // 2. Paste: (default) handle case when focus is somewhere else. + PasteAction.addImplementation(0, (accessor: ServicesAccessor, args: any) => { + document.execCommand('paste'); + return true; + }); +} if (supportsCopyWithSyntaxHighlighting) { registerEditorAction(ExecCommandCopyWithSyntaxHighlightingAction); diff --git a/src/vs/editor/contrib/codeAction/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/codeActionCommands.ts index 4acfb917796..699e05a4699 100644 --- a/src/vs/editor/contrib/codeAction/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/codeActionCommands.ts @@ -11,7 +11,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, EditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditService, ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; import { IPosition } from 'vs/editor/common/core/position'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; @@ -163,7 +163,7 @@ export async function applyCodeAction( }); if (action.edit) { - await bulkEditService.apply(action.edit, { editor, label: action.title }); + await bulkEditService.apply(ResourceEdit.convert(action.edit), { editor, label: action.title }); } if (action.command) { diff --git a/src/vs/editor/contrib/codelens/codelensController.ts b/src/vs/editor/contrib/codelens/codelensController.ts index 6989f44cb12..14e21796a7c 100644 --- a/src/vs/editor/contrib/codelens/codelensController.ts +++ b/src/vs/editor/contrib/codelens/codelensController.ts @@ -95,7 +95,7 @@ export class CodeLensContribution implements IEditorContribution { .monaco-editor .codelens-decoration.${this._styleClassName} { height: ${height}px; line-height: ${lineHeight}px; font-size: ${fontSize}px; padding-right: ${Math.round(fontInfo.fontSize * 0.45)}px;} .monaco-editor .codelens-decoration.${this._styleClassName} > a > .codicon { line-height: ${lineHeight}px; font-size: ${fontSize}px; } `; - this._styleElement.innerHTML = newStyle; + this._styleElement.textContent = newStyle; } private _localDispose(): void { @@ -470,5 +470,3 @@ registerEditorAction(class ShowLensesInCurrentLine extends EditorAction { } } }); - - diff --git a/src/vs/editor/contrib/codelens/codelensWidget.ts b/src/vs/editor/contrib/codelens/codelensWidget.ts index 6fe58d4ee80..8c05d953292 100644 --- a/src/vs/editor/contrib/codelens/codelensWidget.ts +++ b/src/vs/editor/contrib/codelens/codelensWidget.ts @@ -108,7 +108,7 @@ class CodeLensContentWidget implements IContentWidget { } else { // symbols and commands if (!innerHtml) { - innerHtml = ' '; + innerHtml = '\u00a0'; } this._domNode.innerHTML = innerHtml; if (this._isEmpty && animate) { diff --git a/src/vs/editor/contrib/find/findController.ts b/src/vs/editor/contrib/find/findController.ts index 2730f6b8040..7e452f6a6cd 100644 --- a/src/vs/editor/contrib/find/findController.ts +++ b/src/vs/editor/contrib/find/findController.ts @@ -20,7 +20,6 @@ import { MenuId } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { optional } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; @@ -233,12 +232,22 @@ export class CommonFindController extends Disposable implements IEditorContribut this._state.change({ searchScope: null }, true); } else { if (this._editor.hasModel()) { - let selection = this._editor.getSelection(); - if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) { - selection = selection.setEndPosition(selection.endLineNumber - 1, this._editor.getModel().getLineMaxColumn(selection.endLineNumber - 1)); - } - if (!selection.isEmpty()) { - this._state.change({ searchScope: selection }, true); + let selections = this._editor.getSelections(); + selections.map(selection => { + if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) { + selection = selection.setEndPosition( + selection.endLineNumber - 1, + this._editor.getModel()!.getLineMaxColumn(selection.endLineNumber - 1) + ); + } + if (!selection.isEmpty()) { + return selection; + } + return null; + }).filter(element => !!element); + + if (selections.length) { + this._state.change({ searchScope: selections }, true); } } } @@ -299,9 +308,9 @@ export class CommonFindController extends Disposable implements IEditorContribut } if (opts.updateSearchScope) { - let currentSelection = this._editor.getSelection(); - if (!currentSelection.isEmpty()) { - stateChanges.searchScope = currentSelection; + let currentSelections = this._editor.getSelections(); + if (currentSelections.some(selection => !selection.isEmpty())) { + stateChanges.searchScope = currentSelections; } } @@ -361,7 +370,6 @@ export class CommonFindController extends Disposable implements IEditorContribut public async getGlobalBufferTerm(): Promise { if (this._editor.getOption(EditorOption.find).globalFindClipboard - && this._clipboardService && this._editor.hasModel() && !this._editor.getModel().isTooLargeForSyncing() ) { @@ -372,7 +380,6 @@ export class CommonFindController extends Disposable implements IEditorContribut public setGlobalBufferTerm(text: string): void { if (this._editor.getOption(EditorOption.find).globalFindClipboard - && this._clipboardService && this._editor.hasModel() && !this._editor.getModel().isTooLargeForSyncing() ) { @@ -396,7 +403,7 @@ export class FindController extends CommonFindController implements IFindControl @INotificationService private readonly _notificationService: INotificationService, @IStorageService _storageService: IStorageService, @IStorageKeysSyncRegistryService private readonly _storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, - @optional(IClipboardService) clipboardService: IClipboardService, + @IClipboardService clipboardService: IClipboardService, ) { super(editor, _contextKeyService, _storageService, clipboardService); this._widget = null; diff --git a/src/vs/editor/contrib/find/findDecorations.ts b/src/vs/editor/contrib/find/findDecorations.ts index a3f137d1575..c6b966e77c1 100644 --- a/src/vs/editor/contrib/find/findDecorations.ts +++ b/src/vs/editor/contrib/find/findDecorations.ts @@ -17,7 +17,7 @@ export class FindDecorations implements IDisposable { private readonly _editor: IActiveCodeEditor; private _decorations: string[]; private _overviewRulerApproximateDecorations: string[]; - private _findScopeDecorationId: string | null; + private _findScopeDecorationIds: string[]; private _rangeHighlightDecorationId: string | null; private _highlightedDecorationId: string | null; private _startPosition: Position; @@ -26,7 +26,7 @@ export class FindDecorations implements IDisposable { this._editor = editor; this._decorations = []; this._overviewRulerApproximateDecorations = []; - this._findScopeDecorationId = null; + this._findScopeDecorationIds = []; this._rangeHighlightDecorationId = null; this._highlightedDecorationId = null; this._startPosition = this._editor.getPosition(); @@ -37,7 +37,7 @@ export class FindDecorations implements IDisposable { this._decorations = []; this._overviewRulerApproximateDecorations = []; - this._findScopeDecorationId = null; + this._findScopeDecorationIds = []; this._rangeHighlightDecorationId = null; this._highlightedDecorationId = null; } @@ -45,7 +45,7 @@ export class FindDecorations implements IDisposable { public reset(): void { this._decorations = []; this._overviewRulerApproximateDecorations = []; - this._findScopeDecorationId = null; + this._findScopeDecorationIds = []; this._rangeHighlightDecorationId = null; this._highlightedDecorationId = null; } @@ -54,9 +54,22 @@ export class FindDecorations implements IDisposable { return this._decorations.length; } + /** @deprecated use getFindScopes to support multiple selections */ public getFindScope(): Range | null { - if (this._findScopeDecorationId) { - return this._editor.getModel().getDecorationRange(this._findScopeDecorationId); + if (this._findScopeDecorationIds[0]) { + return this._editor.getModel().getDecorationRange(this._findScopeDecorationIds[0]); + } + return null; + } + + public getFindScopes(): Range[] | null { + if (this._findScopeDecorationIds.length) { + const scopes = this._findScopeDecorationIds.map(findScopeDecorationId => + this._editor.getModel().getDecorationRange(findScopeDecorationId) + ).filter(element => !!element); + if (scopes.length) { + return scopes as Range[]; + } } return null; } @@ -133,7 +146,7 @@ export class FindDecorations implements IDisposable { return matchPosition; } - public set(findMatches: FindMatch[], findScope: Range | null): void { + public set(findMatches: FindMatch[], findScopes: Range[] | null): void { this._editor.changeDecorations((accessor) => { let findMatchesOptions: ModelDecorationOptions = FindDecorations._FIND_MATCH_DECORATION; @@ -195,12 +208,12 @@ export class FindDecorations implements IDisposable { } // Find scope - if (this._findScopeDecorationId) { - accessor.removeDecoration(this._findScopeDecorationId); - this._findScopeDecorationId = null; + if (this._findScopeDecorationIds.length) { + this._findScopeDecorationIds.forEach(findScopeDecorationId => accessor.removeDecoration(findScopeDecorationId)); + this._findScopeDecorationIds = []; } - if (findScope) { - this._findScopeDecorationId = accessor.addDecoration(findScope, FindDecorations._FIND_SCOPE_DECORATION); + if (findScopes?.length) { + this._findScopeDecorationIds = findScopes.map(findScope => accessor.addDecoration(findScope, FindDecorations._FIND_SCOPE_DECORATION)); } }); } @@ -253,8 +266,8 @@ export class FindDecorations implements IDisposable { let result: string[] = []; result = result.concat(this._decorations); result = result.concat(this._overviewRulerApproximateDecorations); - if (this._findScopeDecorationId) { - result.push(this._findScopeDecorationId); + if (this._findScopeDecorationIds.length) { + result.push(...this._findScopeDecorationIds); } if (this._rangeHighlightDecorationId) { result.push(this._rangeHighlightDecorationId); diff --git a/src/vs/editor/contrib/find/findModel.ts b/src/vs/editor/contrib/find/findModel.ts index eca1f89f2e7..a902f36c6d0 100644 --- a/src/vs/editor/contrib/find/findModel.ts +++ b/src/vs/editor/contrib/find/findModel.ts @@ -169,26 +169,36 @@ export class FindModelBoundToEditorModel { return model.getFullModelRange(); } - private research(moveCursor: boolean, newFindScope?: Range | null): void { - let findScope: Range | null = null; + private research(moveCursor: boolean, newFindScope?: Range | Range[] | null): void { + let findScopes: Range[] | null = null; if (typeof newFindScope !== 'undefined') { - findScope = newFindScope; - } else { - findScope = this._decorations.getFindScope(); - } - if (findScope !== null) { - if (findScope.startLineNumber !== findScope.endLineNumber) { - if (findScope.endColumn === 1) { - findScope = new Range(findScope.startLineNumber, 1, findScope.endLineNumber - 1, this._editor.getModel().getLineMaxColumn(findScope.endLineNumber - 1)); + if (newFindScope !== null) { + if (!Array.isArray(newFindScope)) { + findScopes = [newFindScope as Range]; } else { - // multiline find scope => expand to line starts / ends - findScope = new Range(findScope.startLineNumber, 1, findScope.endLineNumber, this._editor.getModel().getLineMaxColumn(findScope.endLineNumber)); + findScopes = newFindScope; } } + } else { + findScopes = this._decorations.getFindScopes(); + } + if (findScopes !== null) { + findScopes = findScopes.map(findScope => { + if (findScope.startLineNumber !== findScope.endLineNumber) { + let endLineNumber = findScope.endLineNumber; + + if (findScope.endColumn === 1) { + endLineNumber = endLineNumber - 1; + } + + return new Range(findScope.startLineNumber, 1, endLineNumber, this._editor.getModel().getLineMaxColumn(endLineNumber)); + } + return findScope; + }); } - let findMatches = this._findMatches(findScope, false, MATCHES_LIMIT); - this._decorations.set(findMatches, findScope); + let findMatches = this._findMatches(findScopes, false, MATCHES_LIMIT); + this._decorations.set(findMatches, findScopes); const editorSelection = this._editor.getSelection(); let currentMatchesPosition = this._decorations.getCurrentMatchesPosition(editorSelection); @@ -205,7 +215,7 @@ export class FindModelBoundToEditorModel { undefined ); - if (moveCursor) { + if (moveCursor && this._editor.getOption(EditorOption.find).cursorMoveOnType) { this._moveToNextMatch(this._decorations.getStartPosition()); } } @@ -467,9 +477,12 @@ export class FindModelBoundToEditorModel { } } - private _findMatches(findScope: Range | null, captureMatches: boolean, limitResultCount: number): FindMatch[] { - let searchRange = FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), findScope); - return this._editor.getModel().findMatches(this._state.searchString, searchRange, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(EditorOption.wordSeparators) : null, captureMatches, limitResultCount); + private _findMatches(findScopes: Range[] | null, captureMatches: boolean, limitResultCount: number): FindMatch[] { + const searchRanges = (findScopes as [] || [null]).map((scope: Range | null) => + FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), scope) + ); + + return this._editor.getModel().findMatches(this._state.searchString, searchRanges, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(EditorOption.wordSeparators) : null, captureMatches, limitResultCount); } public replaceAll(): void { @@ -477,13 +490,13 @@ export class FindModelBoundToEditorModel { return; } - const findScope = this._decorations.getFindScope(); + const findScopes = this._decorations.getFindScopes(); - if (findScope === null && this._state.matchesCount >= MATCHES_LIMIT) { + if (findScopes === null && this._state.matchesCount >= MATCHES_LIMIT) { // Doing a replace on the entire file that is over ${MATCHES_LIMIT} matches this._largeReplaceAll(); } else { - this._regularReplaceAll(findScope); + this._regularReplaceAll(findScopes); } this.research(false); @@ -528,10 +541,10 @@ export class FindModelBoundToEditorModel { this._executeEditorCommand('replaceAll', command); } - private _regularReplaceAll(findScope: Range | null): void { + private _regularReplaceAll(findScopes: Range[] | null): void { const replacePattern = this._getReplacePattern(); // Get all the ranges (even more than the highlighted ones) - let matches = this._findMatches(findScope, replacePattern.hasReplacementPatterns || this._state.preserveCase, Constants.MAX_SAFE_SMALL_INTEGER); + let matches = this._findMatches(findScopes, replacePattern.hasReplacementPatterns || this._state.preserveCase, Constants.MAX_SAFE_SMALL_INTEGER); let replaceStrings: string[] = []; for (let i = 0, len = matches.length; i < len; i++) { @@ -547,10 +560,10 @@ export class FindModelBoundToEditorModel { return; } - let findScope = this._decorations.getFindScope(); + let findScopes = this._decorations.getFindScopes(); // Get all the ranges (even more than the highlighted ones) - let matches = this._findMatches(findScope, false, Constants.MAX_SAFE_SMALL_INTEGER); + let matches = this._findMatches(findScopes, false, Constants.MAX_SAFE_SMALL_INTEGER); let selections = matches.map(m => new Selection(m.range.startLineNumber, m.range.startColumn, m.range.endLineNumber, m.range.endColumn)); // If one of the ranges is the editor selection, then maintain it as primary diff --git a/src/vs/editor/contrib/find/findState.ts b/src/vs/editor/contrib/find/findState.ts index dbadf2bb664..0313dd8bc46 100644 --- a/src/vs/editor/contrib/find/findState.ts +++ b/src/vs/editor/contrib/find/findState.ts @@ -46,7 +46,7 @@ export interface INewFindReplaceState { matchCaseOverride?: FindOptionOverride; preserveCase?: boolean; preserveCaseOverride?: FindOptionOverride; - searchScope?: Range | null; + searchScope?: Range[] | null; loop?: boolean; } @@ -73,7 +73,7 @@ export class FindReplaceState extends Disposable { private _matchCaseOverride: FindOptionOverride; private _preserveCase: boolean; private _preserveCaseOverride: FindOptionOverride; - private _searchScope: Range | null; + private _searchScope: Range[] | null; private _matchesPosition: number; private _matchesCount: number; private _currentMatch: Range | null; @@ -94,7 +94,7 @@ export class FindReplaceState extends Disposable { public get actualMatchCase(): boolean { return this._matchCase; } public get actualPreserveCase(): boolean { return this._preserveCase; } - public get searchScope(): Range | null { return this._searchScope; } + public get searchScope(): Range[] | null { return this._searchScope; } public get matchesPosition(): number { return this._matchesPosition; } public get matchesCount(): number { return this._matchesCount; } public get currentMatch(): Range | null { return this._currentMatch; } @@ -238,7 +238,11 @@ export class FindReplaceState extends Disposable { this._preserveCase = newState.preserveCase; } if (typeof newState.searchScope !== 'undefined') { - if (!Range.equalsRange(this._searchScope, newState.searchScope)) { + if (!newState.searchScope?.every((newSearchScope) => { + return this._searchScope?.some(existingSearchScope => { + return !Range.equalsRange(existingSearchScope, newSearchScope); + }); + })) { this._searchScope = newState.searchScope; changeEvent.searchScope = true; somethingChanged = true; diff --git a/src/vs/editor/contrib/find/findWidget.ts b/src/vs/editor/contrib/find/findWidget.ts index 82af435546f..9ba13e5aff5 100644 --- a/src/vs/editor/contrib/find/findWidget.ts +++ b/src/vs/editor/contrib/find/findWidget.ts @@ -804,16 +804,26 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL } if (this._toggleSelectionFind.checked) { - let selection = this._codeEditor.getSelection(); - if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) { - selection = selection.setEndPosition(selection.endLineNumber - 1, this._codeEditor.getModel().getLineMaxColumn(selection.endLineNumber - 1)); - } - const currentMatch = this._state.currentMatch; - if (selection.startLineNumber !== selection.endLineNumber) { - if (!Range.equalsRange(selection, currentMatch)) { - // Reseed find scope - this._state.change({ searchScope: selection }, true); + let selections = this._codeEditor.getSelections(); + + selections.map(selection => { + if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) { + selection = selection.setEndPosition( + selection.endLineNumber - 1, + this._codeEditor.getModel()!.getLineMaxColumn(selection.endLineNumber - 1) + ); } + const currentMatch = this._state.currentMatch; + if (selection.startLineNumber !== selection.endLineNumber) { + if (!Range.equalsRange(selection, currentMatch)) { + return selection; + } + } + return null; + }).filter(element => !!element); + + if (selections.length) { + this._state.change({ searchScope: selections as Range[] }, true); } } } @@ -1028,12 +1038,19 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL this._register(this._toggleSelectionFind.onChange(() => { if (this._toggleSelectionFind.checked) { if (this._codeEditor.hasModel()) { - let selection = this._codeEditor.getSelection(); - if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) { - selection = selection.setEndPosition(selection.endLineNumber - 1, this._codeEditor.getModel().getLineMaxColumn(selection.endLineNumber - 1)); - } - if (!selection.isEmpty()) { - this._state.change({ searchScope: selection }, true); + let selections = this._codeEditor.getSelections(); + selections.map(selection => { + if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) { + selection = selection.setEndPosition(selection.endLineNumber - 1, this._codeEditor.getModel()!.getLineMaxColumn(selection.endLineNumber - 1)); + } + if (!selection.isEmpty()) { + return selection; + } + return null; + }).filter(element => !!element); + + if (selections.length) { + this._state.change({ searchScope: selections as Range[] }, true); } } } else { diff --git a/src/vs/editor/contrib/find/test/findController.test.ts b/src/vs/editor/contrib/find/test/findController.test.ts index eaac102b78c..ee0c4862632 100644 --- a/src/vs/editor/contrib/find/test/findController.test.ts +++ b/src/vs/editor/contrib/find/test/findController.test.ts @@ -309,10 +309,10 @@ suite('FindController', async () => { assert.equal(findController.getState().searchScope, null); findController.getState().change({ - searchScope: new Range(1, 1, 1, 5) + searchScope: [new Range(1, 1, 1, 5)] }, false); - assert.deepEqual(findController.getState().searchScope, new Range(1, 1, 1, 5)); + assert.deepEqual(findController.getState().searchScope, [new Range(1, 1, 1, 5)]); findController.closeFindWidget(); assert.equal(findController.getState().searchScope, null); @@ -523,10 +523,8 @@ suite('FindController query options persistence', async () => { 'var z = (3 * 5)', ], { serviceCollection: serviceCollection, find: { autoFindInSelection: 'always', globalFindClipboard: false } }, async (editor) => { // clipboardState = ''; - editor.setSelection(new Range(1, 1, 2, 1)); let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); - - await findController.start({ + const findConfig = { forceRevealReplace: false, seedSearchStringFromSelection: false, seedSearchStringFromGlobalClipboard: false, @@ -534,9 +532,17 @@ suite('FindController query options persistence', async () => { shouldAnimate: false, updateSearchScope: true, loop: true - }); + }; - assert.deepEqual(findController.getState().searchScope, new Selection(1, 1, 2, 1)); + editor.setSelection(new Range(1, 1, 2, 1)); + findController.start(findConfig); + assert.deepEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1)]); + + findController.closeFindWidget(); + + editor.setSelections([new Selection(1, 1, 2, 1), new Selection(2, 1, 2, 5)]); + findController.start(findConfig); + assert.deepEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1), new Selection(2, 1, 2, 5)]); }); }); @@ -584,7 +590,7 @@ suite('FindController query options persistence', async () => { loop: true }); - assert.deepEqual(findController.getState().searchScope, new Selection(1, 2, 1, 3)); + assert.deepEqual(findController.getState().searchScope, [new Selection(1, 2, 1, 3)]); }); }); @@ -609,7 +615,7 @@ suite('FindController query options persistence', async () => { loop: true }); - assert.deepEqual(findController.getState().searchScope, new Selection(1, 6, 2, 1)); + assert.deepEqual(findController.getState().searchScope, [new Selection(1, 6, 2, 1)]); }); }); }); diff --git a/src/vs/editor/contrib/find/test/findModel.test.ts b/src/vs/editor/contrib/find/test/findModel.test.ts index 1a515fdddcc..70c5494985a 100644 --- a/src/vs/editor/contrib/find/test/findModel.test.ts +++ b/src/vs/editor/contrib/find/test/findModel.test.ts @@ -210,7 +210,7 @@ suite('FindModel', () => { ); // simulate adding a search scope - findState.change({ searchScope: new Range(8, 1, 10, 1) }, true); + findState.change({ searchScope: [new Range(8, 1, 10, 1)] }, true); assertFindState( editor, [8, 14, 8, 19], @@ -443,7 +443,7 @@ suite('FindModel', () => { findTest('find model next stays in scope', (editor) => { let findState = new FindReplaceState(); - findState.change({ searchString: 'hello', wholeWord: true, searchScope: new Range(7, 1, 9, 1) }, false); + findState.change({ searchString: 'hello', wholeWord: true, searchScope: [new Range(7, 1, 9, 1)] }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); assertFindState( @@ -493,6 +493,131 @@ suite('FindModel', () => { findState.dispose(); }); + findTest('multi-selection find model next stays in scope (overlap)', (editor) => { + let findState = new FindReplaceState(); + findState.change({ searchString: 'hello', wholeWord: true, searchScope: [new Range(7, 1, 8, 2), new Range(8, 1, 9, 1)] }, false); + let findModel = new FindModelBoundToEditorModel(editor, findState); + + assertFindState( + editor, + [1, 1, 1, 1], + null, + [ + [7, 14, 7, 19], + [8, 14, 8, 19] + ] + ); + + findModel.moveToNextMatch(); + assertFindState( + editor, + [7, 14, 7, 19], + [7, 14, 7, 19], + [ + [7, 14, 7, 19], + [8, 14, 8, 19] + ] + ); + + findModel.moveToNextMatch(); + assertFindState( + editor, + [8, 14, 8, 19], + [8, 14, 8, 19], + [ + [7, 14, 7, 19], + [8, 14, 8, 19] + ] + ); + + findModel.moveToNextMatch(); + assertFindState( + editor, + [7, 14, 7, 19], + [7, 14, 7, 19], + [ + [7, 14, 7, 19], + [8, 14, 8, 19] + ] + ); + + findModel.dispose(); + findState.dispose(); + }); + + findTest('multi-selection find model next stays in scope', (editor) => { + let findState = new FindReplaceState(); + findState.change({ searchString: 'hello', matchCase: true, wholeWord: false, searchScope: [new Range(6, 1, 7, 38), new Range(9, 3, 9, 38)] }, false); + let findModel = new FindModelBoundToEditorModel(editor, findState); + + assertFindState( + editor, + [1, 1, 1, 1], + null, + [ + [6, 14, 6, 19], + // `matchCase: false` would + // find this match as well: + // [6, 27, 6, 32], + [7, 14, 7, 19], + // `wholeWord: true` would + // exclude this match: + [9, 14, 9, 19], + ] + ); + + findModel.moveToNextMatch(); + assertFindState( + editor, + [6, 14, 6, 19], + [6, 14, 6, 19], + [ + [6, 14, 6, 19], + [7, 14, 7, 19], + [9, 14, 9, 19], + ] + ); + + findModel.moveToNextMatch(); + assertFindState( + editor, + [7, 14, 7, 19], + [7, 14, 7, 19], + [ + [6, 14, 6, 19], + [7, 14, 7, 19], + [9, 14, 9, 19], + ] + ); + + findModel.moveToNextMatch(); + assertFindState( + editor, + [9, 14, 9, 19], + [9, 14, 9, 19], + [ + [6, 14, 6, 19], + [7, 14, 7, 19], + [9, 14, 9, 19], + ] + ); + + findModel.moveToNextMatch(); + assertFindState( + editor, + [6, 14, 6, 19], + [6, 14, 6, 19], + [ + [6, 14, 6, 19], + [7, 14, 7, 19], + [9, 14, 9, 19], + ] + ); + + findModel.dispose(); + findState.dispose(); + }); + findTest('find model prev', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello', wholeWord: true }, false); @@ -581,7 +706,7 @@ suite('FindModel', () => { findTest('find model prev stays in scope', (editor) => { let findState = new FindReplaceState(); - findState.change({ searchString: 'hello', wholeWord: true, searchScope: new Range(7, 1, 9, 1) }, false); + findState.change({ searchString: 'hello', wholeWord: true, searchScope: [new Range(7, 1, 9, 1)] }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); assertFindState( @@ -2073,7 +2198,7 @@ suite('FindModel', () => { findTest('issue #27083. search scope works even if it is a single line', (editor) => { let findState = new FindReplaceState(); - findState.change({ searchString: 'hello', wholeWord: true, searchScope: new Range(7, 1, 8, 1) }, false); + findState.change({ searchString: 'hello', wholeWord: true, searchScope: [new Range(7, 1, 8, 1)] }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); assertFindState( diff --git a/src/vs/editor/contrib/format/format.ts b/src/vs/editor/contrib/format/format.ts index 2f619c371d8..df4b5cdcd4f 100644 --- a/src/vs/editor/contrib/format/format.ts +++ b/src/vs/editor/contrib/format/format.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { alert } from 'vs/base/browser/ui/aria/aria'; -import { isNonEmptyArray } from 'vs/base/common/arrays'; +import { asArray, isNonEmptyArray } from 'vs/base/common/arrays'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; @@ -120,11 +120,12 @@ export abstract class FormattingConflicts { } } -export async function formatDocumentRangeWithSelectedProvider( +export async function formatDocumentRangesWithSelectedProvider( accessor: ServicesAccessor, editorOrModel: ITextModel | IActiveCodeEditor, - range: Range, + rangeOrRanges: Range | Range[], mode: FormattingMode, + progress: IProgress, token: CancellationToken ): Promise { @@ -133,15 +134,16 @@ export async function formatDocumentRangeWithSelectedProvider( const provider = DocumentRangeFormattingEditProviderRegistry.ordered(model); const selected = await FormattingConflicts.select(provider, model, mode); if (selected) { - await instaService.invokeFunction(formatDocumentRangeWithProvider, selected, editorOrModel, range, token); + progress.report(selected); + await instaService.invokeFunction(formatDocumentRangesWithProvider, selected, editorOrModel, rangeOrRanges, token); } } -export async function formatDocumentRangeWithProvider( +export async function formatDocumentRangesWithProvider( accessor: ServicesAccessor, provider: DocumentRangeFormattingEditProvider, editorOrModel: ITextModel | IActiveCodeEditor, - range: Range, + rangeOrRanges: Range | Range[], token: CancellationToken ): Promise { const workerService = accessor.get(IEditorWorkerService); @@ -156,39 +158,53 @@ export async function formatDocumentRangeWithProvider( cts = new TextModelCancellationTokenSource(editorOrModel, token); } - let edits: TextEdit[] | undefined; - try { - const rawEdits = await provider.provideDocumentRangeFormattingEdits( - model, - range, - model.getFormattingOptions(), - cts.token - ); - edits = await workerService.computeMoreMinimalEdits(model.uri, rawEdits); - - if (cts.token.isCancellationRequested) { - return true; + // make sure that ranges don't overlap nor touch each other + let ranges: Range[] = []; + let len = 0; + for (let range of asArray(rangeOrRanges).sort(Range.compareRangesUsingStarts)) { + if (len > 0 && Range.areIntersectingOrTouching(ranges[len - 1], range)) { + ranges[len - 1] = Range.fromPositions(ranges[len - 1].getStartPosition(), range.getEndPosition()); + } else { + len = ranges.push(range); } - - } finally { - cts.dispose(); } - if (!edits || edits.length === 0) { + const allEdits: TextEdit[] = []; + for (let range of ranges) { + try { + const rawEdits = await provider.provideDocumentRangeFormattingEdits( + model, + range, + model.getFormattingOptions(), + cts.token + ); + const minEdits = await workerService.computeMoreMinimalEdits(model.uri, rawEdits); + if (minEdits) { + allEdits.push(...minEdits); + } + if (cts.token.isCancellationRequested) { + return true; + } + } finally { + cts.dispose(); + } + } + + if (allEdits.length === 0) { return false; } if (isCodeEditor(editorOrModel)) { // use editor to apply edits - FormattingEdit.execute(editorOrModel, edits, true); - alertFormattingEdits(edits); + FormattingEdit.execute(editorOrModel, allEdits, true); + alertFormattingEdits(allEdits); editorOrModel.revealPositionInCenterIfOutsideViewport(editorOrModel.getPosition(), ScrollType.Immediate); } else { // use model to apply edits - const [{ range }] = edits; + const [{ range }] = allEdits; const initialSelection = new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); - model.pushEditOperations([initialSelection], edits.map(edit => { + model.pushEditOperations([initialSelection], allEdits.map(edit => { return { text: edit.text, range: Range.lift(edit.range), diff --git a/src/vs/editor/contrib/format/formatActions.ts b/src/vs/editor/contrib/format/formatActions.ts index c2eed67eab0..bedb20cdd97 100644 --- a/src/vs/editor/contrib/format/formatActions.ts +++ b/src/vs/editor/contrib/format/formatActions.ts @@ -16,7 +16,7 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { DocumentRangeFormattingEditProviderRegistry, OnTypeFormattingEditProviderRegistry } from 'vs/editor/common/modes'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; -import { getOnTypeFormattingEdits, alertFormattingEdits, formatDocumentRangeWithSelectedProvider, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; +import { getOnTypeFormattingEdits, alertFormattingEdits, formatDocumentRangesWithSelectedProvider, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; import { FormattingEdit } from 'vs/editor/contrib/format/formattingEdit'; import * as nls from 'vs/nls'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; @@ -25,7 +25,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { onUnexpectedError } from 'vs/base/common/errors'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { Progress } from 'vs/platform/progress/common/progress'; +import { Progress, IEditorProgressService } from 'vs/platform/progress/common/progress'; class FormatOnType implements IEditorContribution { @@ -202,7 +202,7 @@ class FormatOnPaste implements IEditorContribution { if (this.editor.getSelections().length > 1) { return; } - this._instantiationService.invokeFunction(formatDocumentRangeWithSelectedProvider, this.editor, range, FormattingMode.Silent, CancellationToken.None).catch(onUnexpectedError); + this._instantiationService.invokeFunction(formatDocumentRangesWithSelectedProvider, this.editor, range, FormattingMode.Silent, Progress.None, CancellationToken.None).catch(onUnexpectedError); } } @@ -231,7 +231,11 @@ class FormatDocumentAction extends EditorAction { async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { if (editor.hasModel()) { const instaService = accessor.get(IInstantiationService); - await instaService.invokeFunction(formatDocumentWithSelectedProvider, editor, FormattingMode.Explicit, Progress.None, CancellationToken.None); + const progressService = accessor.get(IEditorProgressService); + await progressService.showWhile( + instaService.invokeFunction(formatDocumentWithSelectedProvider, editor, FormattingMode.Explicit, Progress.None, CancellationToken.None), + 250 + ); } } } @@ -263,11 +267,18 @@ class FormatSelectionAction extends EditorAction { } const instaService = accessor.get(IInstantiationService); const model = editor.getModel(); - let range: Range = editor.getSelection(); - if (range.isEmpty()) { - range = new Range(range.startLineNumber, 1, range.startLineNumber, model.getLineMaxColumn(range.startLineNumber)); - } - await instaService.invokeFunction(formatDocumentRangeWithSelectedProvider, editor, range, FormattingMode.Explicit, CancellationToken.None); + + const ranges = editor.getSelections().map(range => { + return range.isEmpty() + ? new Range(range.startLineNumber, 1, range.startLineNumber, model.getLineMaxColumn(range.startLineNumber)) + : range; + }); + + const progressService = accessor.get(IEditorProgressService); + await progressService.showWhile( + instaService.invokeFunction(formatDocumentRangesWithSelectedProvider, editor, ranges, FormattingMode.Explicit, Progress.None, CancellationToken.None), + 250 + ); } } diff --git a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts index 01a7dc55aa0..f5b6ec41d4f 100644 --- a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts @@ -167,7 +167,7 @@ class MessageWidget { let relatedResource = document.createElement('a'); dom.addClass(relatedResource, 'filename'); - relatedResource.innerHTML = `${getBaseLabel(related.resource)}(${related.startLineNumber}, ${related.startColumn}): `; + relatedResource.innerText = `${getBaseLabel(related.resource)}(${related.startLineNumber}, ${related.startColumn}): `; relatedResource.title = getPathLabel(related.resource, undefined); this._relatedDiagnostics.set(relatedResource, related); diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts index 7d429358a35..86e708fbdc8 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts @@ -429,7 +429,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { if (this._model.isEmpty) { this.setTitle(''); - this._messageContainer.innerHTML = nls.localize('noResults', "No results"); + this._messageContainer.innerText = nls.localize('noResults', "No results"); dom.show(this._messageContainer); return Promise.resolve(undefined); } diff --git a/src/vs/editor/contrib/multicursor/multicursor.ts b/src/vs/editor/contrib/multicursor/multicursor.ts index cdb7d0c6190..6a7c295e827 100644 --- a/src/vs/editor/contrib/multicursor/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/multicursor.ts @@ -601,13 +601,15 @@ export class MultiCursorSelectionController extends Disposable implements IEdito } if (findState.searchScope) { - const state = findState.searchScope; + const states = findState.searchScope; let inSelection: FindMatch[] | null = []; - for (let i = 0; i < matches.length; i++) { - if (matches[i].range.endLineNumber <= state.endLineNumber && matches[i].range.startLineNumber >= state.startLineNumber) { - inSelection.push(matches[i]); - } - } + matches.forEach((match) => { + states.forEach((state) => { + if (match.range.endLineNumber <= state.endLineNumber && match.range.startLineNumber >= state.startLineNumber) { + inSelection!.push(match); + } + }); + }); matches = inSelection; } @@ -969,7 +971,7 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut return; } - const hasFindOccurrences = DocumentHighlightProviderRegistry.has(model); + const hasFindOccurrences = DocumentHighlightProviderRegistry.has(model) && this.editor.getOption(EditorOption.occurrencesHighlight); let allMatches = model.findMatches(this.state.searchText, true, false, this.state.matchCase, this.state.wordSeparators, false).map(m => m.range); allMatches.sort(Range.compareRangesUsingStarts); diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts index e140bdc4cd5..c34c98c2f97 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts @@ -194,8 +194,8 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { dom.toggleClass(this.domNodes.element, 'multiple', multiple); this.keyMultipleSignatures.set(multiple); - this.domNodes.signature.innerHTML = ''; - this.domNodes.docs.innerHTML = ''; + this.domNodes.signature.innerText = ''; + this.domNodes.docs.innerText = ''; const signature = hints.signatures[hints.activeSignature]; if (!signature) { diff --git a/src/vs/editor/contrib/peekView/peekView.ts b/src/vs/editor/contrib/peekView/peekView.ts index cbada7a1e48..f265a4bfd60 100644 --- a/src/vs/editor/contrib/peekView/peekView.ts +++ b/src/vs/editor/contrib/peekView/peekView.ts @@ -11,7 +11,6 @@ import { Action } from 'vs/base/common/actions'; import { Color } from 'vs/base/common/color'; import { Emitter } from 'vs/base/common/event'; import * as objects from 'vs/base/common/objects'; -import * as strings from 'vs/base/common/strings'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; @@ -223,10 +222,10 @@ export abstract class PeekViewWidget extends ZoneWidget { setTitle(primaryHeading: string, secondaryHeading?: string): void { if (this._primaryHeading && this._secondaryHeading) { - this._primaryHeading.innerHTML = strings.escape(primaryHeading); + this._primaryHeading.innerText = primaryHeading; this._primaryHeading.setAttribute('aria-label', primaryHeading); if (secondaryHeading) { - this._secondaryHeading.innerHTML = strings.escape(secondaryHeading); + this._secondaryHeading.innerText = secondaryHeading; } else { dom.clearNode(this._secondaryHeading); } @@ -236,7 +235,7 @@ export abstract class PeekViewWidget extends ZoneWidget { setMetaTitle(value: string): void { if (this._metaHeading) { if (value) { - this._metaHeading.innerHTML = strings.escape(value); + this._metaHeading.innerText = value; dom.show(this._metaHeading); } else { dom.hide(this._metaHeading); diff --git a/src/vs/editor/contrib/rename/onTypeRename.ts b/src/vs/editor/contrib/rename/onTypeRename.ts index 51cc31bbefe..105dfb2ddb2 100644 --- a/src/vs/editor/contrib/rename/onTypeRename.ts +++ b/src/vs/editor/contrib/rename/onTypeRename.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import { registerEditorContribution, registerModelAndPositionCommand, EditorAction, EditorCommand, ServicesAccessor, registerEditorAction, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; import * as arrays from 'vs/base/common/arrays'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position, IPosition } from 'vs/editor/common/core/position'; @@ -16,7 +16,7 @@ import { ITextModel, IModelDeltaDecoration, TrackedRangeStickiness, IIdentifiedS import { CancellationToken } from 'vs/base/common/cancellation'; import { IRange, Range } from 'vs/editor/common/core/range'; import { OnTypeRenameProviderRegistry } from 'vs/editor/common/modes'; -import { first, createCancelablePromise, CancelablePromise, RunOnceScheduler } from 'vs/base/common/async'; +import { first, createCancelablePromise, CancelablePromise, Delayer } from 'vs/base/common/async'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { ContextKeyExpr, RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; @@ -24,11 +24,12 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; +import { isPromiseCanceledError, onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; import * as strings from 'vs/base/common/strings'; import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; +import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; export const CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE = new RawContextKey('onTypeRenameInputVisible', false); @@ -45,19 +46,26 @@ export class OnTypeRenameContribution extends Disposable implements IEditorContr return editor.getContribution(OnTypeRenameContribution.ID); } + private _debounceDuration = 200; + private readonly _editor: ICodeEditor; private _enabled: boolean; private readonly _visibleContextKey: IContextKey; - private _currentRequest: CancelablePromise<{ - ranges: IRange[], - stopPattern?: RegExp - } | null | undefined> | null; + private _rangeUpdateTriggerPromise: Promise | null; + private _rangeSyncTriggerPromise: Promise | null; + + private _currentRequest: CancelablePromise | null; + private _currentRequestPosition: Position | null; + private _currentRequestModelVersion: number | null; + private _currentDecorations: string[]; // The one at index 0 is the reference one - private _stopPattern: RegExp; + private _languageWordPattern: RegExp | null; + private _currentWordPattern: RegExp | null; private _ignoreChangeEvent: boolean; - private _updateMirrors: RunOnceScheduler; + + private readonly _localToDispose = this._register(new DisposableStore()); constructor( editor: ICodeEditor, @@ -65,103 +73,117 @@ export class OnTypeRenameContribution extends Disposable implements IEditorContr ) { super(); this._editor = editor; - this._enabled = this._editor.getOption(EditorOption.renameOnType); + this._enabled = false; this._visibleContextKey = CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE.bindTo(contextKeyService); - this._currentRequest = null; + this._currentDecorations = []; - this._stopPattern = /^\s/; + this._languageWordPattern = null; + this._currentWordPattern = null; this._ignoreChangeEvent = false; - this._updateMirrors = this._register(new RunOnceScheduler(() => this._doUpdateMirrors(), 0)); + this._localToDispose = this._register(new DisposableStore()); - this._register(this._editor.onDidChangeModel((e) => { - this.stopAll(); - this.run(); - })); + this._rangeUpdateTriggerPromise = null; + this._rangeSyncTriggerPromise = null; - this._register(this._editor.onDidChangeConfiguration((e) => { + this._currentRequest = null; + this._currentRequestPosition = null; + this._currentRequestModelVersion = null; + + this._register(this._editor.onDidChangeModel(() => this.reinitialize())); + + this._register(this._editor.onDidChangeConfiguration(e => { if (e.hasChanged(EditorOption.renameOnType)) { - this._enabled = this._editor.getOption(EditorOption.renameOnType); - this.stopAll(); - this.run(); + this.reinitialize(); } })); + this._register(OnTypeRenameProviderRegistry.onDidChange(() => this.reinitialize())); + this._register(this._editor.onDidChangeModelLanguage(() => this.reinitialize())); - this._register(this._editor.onDidChangeCursorPosition((e) => { - // no regions, run - if (this._currentDecorations.length === 0) { - this.run(e.position); - } - - // has cached regions, don't run - if (!this._editor.hasModel()) { - return; - } - if (this._currentDecorations.length === 0) { - return; - } - const model = this._editor.getModel(); - const currentRanges = this._currentDecorations.map(decId => model.getDecorationRange(decId)!); - - // just moving cursor around, don't run again - if (Range.containsPosition(currentRanges[0], e.position)) { - return; - } - - // moving cursor out of primary region, run - this.run(e.position); - })); - - this._register(OnTypeRenameProviderRegistry.onDidChange(() => { - this.run(); - })); - - this._register(this._editor.onDidChangeModelContent((e) => { - if (this._ignoreChangeEvent) { - return; - } - if (!this._editor.hasModel()) { - return; - } - if (this._currentDecorations.length === 0) { - // nothing to do - return; - } - if (e.isUndoing || e.isRedoing) { - return; - } - if (e.changes[0] && this._stopPattern.test(e.changes[0].text)) { - this.stopAll(); - return; - } - this._updateMirrors.schedule(); - })); + this.reinitialize(); } - private _doUpdateMirrors(): void { - if (!this._editor.hasModel()) { + private reinitialize() { + const model = this._editor.getModel(); + const isEnabled = model !== null && this._editor.getOption(EditorOption.renameOnType) && OnTypeRenameProviderRegistry.has(model); + if (isEnabled === this._enabled) { return; } - if (this._currentDecorations.length === 0) { + + this._enabled = isEnabled; + + this.clearRanges(); + this._localToDispose.clear(); + + if (!isEnabled || model === null) { + return; + } + + this._languageWordPattern = LanguageConfigurationRegistry.getWordDefinition(model.getLanguageIdentifier().id); + this._localToDispose.add(model.onDidChangeLanguageConfiguration(() => { + this._languageWordPattern = LanguageConfigurationRegistry.getWordDefinition(model.getLanguageIdentifier().id); + })); + + const rangeUpdateScheduler = new Delayer(this._debounceDuration); + const triggerRangeUpdate = () => { + this._rangeUpdateTriggerPromise = rangeUpdateScheduler.trigger(() => this.updateRanges(), this._debounceDuration); + }; + const rangeSyncScheduler = new Delayer(0); + const triggerRangeSync = (decorations: string[]) => { + this._rangeSyncTriggerPromise = rangeSyncScheduler.trigger(() => this._syncRanges(decorations)); + }; + this._localToDispose.add(this._editor.onDidChangeCursorPosition(() => { + triggerRangeUpdate(); + })); + this._localToDispose.add(this._editor.onDidChangeModelContent((e) => { + if (!this._ignoreChangeEvent) { + if (this._currentDecorations.length > 0) { + const referenceRange = model.getDecorationRange(this._currentDecorations[0]); + if (referenceRange && e.changes.every(c => referenceRange.intersectRanges(c.range))) { + triggerRangeSync(this._currentDecorations); + return; + } + } + } + triggerRangeUpdate(); + })); + this._localToDispose.add({ + dispose: () => { + rangeUpdateScheduler.cancel(); + rangeSyncScheduler.cancel(); + } + }); + this.updateRanges(); + } + + private _syncRanges(decorations: string[]): void { + // dalayed invocation, make sure we're still on + if (!this._editor.hasModel() || decorations !== this._currentDecorations || decorations.length === 0) { // nothing to do return; } const model = this._editor.getModel(); - const currentRanges = this._currentDecorations.map(decId => model.getDecorationRange(decId)!); + const referenceRange = model.getDecorationRange(decorations[0]); - const referenceRange = currentRanges[0]; - if (referenceRange.startLineNumber !== referenceRange.endLineNumber) { - return this.stopAll(); + if (!referenceRange || referenceRange.startLineNumber !== referenceRange.endLineNumber) { + return this.clearRanges(); } const referenceValue = model.getValueInRange(referenceRange); - if (this._stopPattern.test(referenceValue)) { - return this.stopAll(); + if (this._currentWordPattern) { + const match = referenceValue.match(this._currentWordPattern); + const matchLength = match ? match[0].length : 0; + if (matchLength !== referenceValue.length) { + return this.clearRanges(); + } } let edits: IIdentifiedSingleEditOperation[] = []; - for (let i = 1, len = currentRanges.length; i < len; i++) { - const mirrorRange = currentRanges[i]; + for (let i = 1, len = decorations.length; i < len; i++) { + const mirrorRange = model.getDecorationRange(decorations[i]); + if (!mirrorRange) { + continue; + } if (mirrorRange.startLineNumber !== mirrorRange.endLineNumber) { edits.push({ range: mirrorRange, @@ -207,72 +229,136 @@ export class OnTypeRenameContribution extends Disposable implements IEditorContr } public dispose(): void { + this.clearRanges(); super.dispose(); - this.stopAll(); } - stopAll(): void { + public clearRanges(): void { this._visibleContextKey.set(false); this._currentDecorations = this._editor.deltaDecorations(this._currentDecorations, []); - } - - async run(position: Position | null = this._editor.getPosition(), force = false): Promise { - if (!position) { - return; - } - if (!this._enabled && !force) { - return; - } - if (!this._editor.hasModel()) { - return; - } - if (this._currentRequest) { this._currentRequest.cancel(); this._currentRequest = null; + this._currentRequestPosition = null; + } + } + + public get currentUpdateTriggerPromise(): Promise { + return this._rangeUpdateTriggerPromise || Promise.resolve(); + } + + public get currentSyncTriggerPromise(): Promise { + return this._rangeSyncTriggerPromise || Promise.resolve(); + } + + public async updateRanges(force = false): Promise { + if (!this._editor.hasModel()) { + this.clearRanges(); + return; + } + + const position = this._editor.getPosition(); + if (!this._enabled && !force || this._editor.getSelections().length > 1) { + // disabled or multicursor + this.clearRanges(); + return; } const model = this._editor.getModel(); - - this._currentRequest = createCancelablePromise(token => getOnTypeRenameRanges(model, position, token)); - try { - const response = await this._currentRequest; - - let ranges: IRange[] = []; - if (response?.ranges) { - ranges = response.ranges; + const modelVersionId = model.getVersionId(); + if (this._currentRequestPosition && this._currentRequestModelVersion === modelVersionId) { + if (position.equals(this._currentRequestPosition)) { + return; // same position } - if (response?.stopPattern) { - this._stopPattern = response.stopPattern; - } - - let foundReferenceRange = false; - for (let i = 0, len = ranges.length; i < len; i++) { - if (Range.containsPosition(ranges[i], position)) { - foundReferenceRange = true; - if (i !== 0) { - const referenceRange = ranges[i]; - ranges.splice(i, 1); - ranges.unshift(referenceRange); - } - break; + if (this._currentDecorations && this._currentDecorations.length > 0) { + const range = model.getDecorationRange(this._currentDecorations[0]); + if (range && range.containsPosition(position)) { + return; // just moving inside the existing primary range } } - - if (!foundReferenceRange) { - // Cannot do on type rename if the ranges are not where the cursor is... - this.stopAll(); - return; - } - - const decorations: IModelDeltaDecoration[] = ranges.map(range => ({ range: range, options: OnTypeRenameContribution.DECORATION })); - this._visibleContextKey.set(true); - this._currentDecorations = this._editor.deltaDecorations(this._currentDecorations, decorations); - } catch (err) { - onUnexpectedError(err); - this.stopAll(); } + + this._currentRequestPosition = position; + this._currentRequestModelVersion = modelVersionId; + const request = createCancelablePromise(async token => { + try { + const response = await getOnTypeRenameRanges(model, position, token); + if (request !== this._currentRequest) { + return; + } + this._currentRequest = null; + if (modelVersionId !== model.getVersionId()) { + return; + } + + let ranges: IRange[] = []; + if (response?.ranges) { + ranges = response.ranges; + } + + this._currentWordPattern = response?.wordPattern || this._languageWordPattern; + + let foundReferenceRange = false; + for (let i = 0, len = ranges.length; i < len; i++) { + if (Range.containsPosition(ranges[i], position)) { + foundReferenceRange = true; + if (i !== 0) { + const referenceRange = ranges[i]; + ranges.splice(i, 1); + ranges.unshift(referenceRange); + } + break; + } + } + + if (!foundReferenceRange) { + // Cannot do on type rename if the ranges are not where the cursor is... + this.clearRanges(); + return; + } + + const decorations: IModelDeltaDecoration[] = ranges.map(range => ({ range: range, options: OnTypeRenameContribution.DECORATION })); + this._visibleContextKey.set(true); + this._currentDecorations = this._editor.deltaDecorations(this._currentDecorations, decorations); + } catch (err) { + if (!isPromiseCanceledError(err)) { + onUnexpectedError(err); + } + if (this._currentRequest === request || !this._currentRequest) { + // stop if we are still the latest request + this.clearRanges(); + } + } + }); + this._currentRequest = request; + return request; } + + // for testing + public setDebounceDuration(timeInMS: number) { + this._debounceDuration = timeInMS; + } + + // private printDecorators(model: ITextModel) { + // return this._currentDecorations.map(d => { + // const range = model.getDecorationRange(d); + // if (range) { + // return this.printRange(range); + // } + // return 'invalid'; + // }).join(','); + // } + + // private printChanges(changes: IModelContentChange[]) { + // return changes.map(c => { + // return `${this.printRange(c.range)} - ${c.text}`; + // } + // ).join(','); + // } + + // private printRange(range: IRange) { + // return `${range.startLineNumber},${range.startColumn}/${range.endLineNumber},${range.endColumn}`; + // } } export class OnTypeRenameAction extends EditorAction { @@ -310,10 +396,10 @@ export class OnTypeRenameAction extends EditorAction { return super.runCommand(accessor, args); } - run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + run(_accessor: ServicesAccessor, editor: ICodeEditor): Promise { const controller = OnTypeRenameContribution.get(editor); if (controller) { - return Promise.resolve(controller.run(editor.getPosition(), true)); + return Promise.resolve(controller.updateRanges(true)); } return Promise.resolve(); } @@ -323,7 +409,7 @@ const OnTypeRenameCommand = EditorCommand.bindToContribution x.stopAll(), + handler: x => x.clearRanges(), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, weight: KeybindingWeight.EditorContrib + 99, @@ -335,7 +421,7 @@ registerEditorCommand(new OnTypeRenameCommand({ export function getOnTypeRenameRanges(model: ITextModel, position: Position, token: CancellationToken): Promise<{ ranges: IRange[], - stopPattern?: RegExp + wordPattern?: RegExp } | undefined | null> { const orderedByScore = OnTypeRenameProviderRegistry.ordered(model); @@ -344,16 +430,16 @@ export function getOnTypeRenameRanges(model: ITextModel, position: Position, tok // (good = none empty array) return first<{ ranges: IRange[], - stopPattern?: RegExp + wordPattern?: RegExp } | undefined>(orderedByScore.map(provider => () => { - return Promise.resolve(provider.provideOnTypeRenameRanges(model, position, token)).then((ranges) => { - if (!ranges) { + return Promise.resolve(provider.provideOnTypeRenameRanges(model, position, token)).then((res) => { + if (!res) { return undefined; } return { - ranges, - stopPattern: provider.stopPattern + ranges: res.ranges, + wordPattern: res.wordPattern || provider.wordPattern }; }, (err) => { onUnexpectedExternalError(err); diff --git a/src/vs/editor/contrib/rename/rename.ts b/src/vs/editor/contrib/rename/rename.ts index 7ae883bcd2e..214d00ffc63 100644 --- a/src/vs/editor/contrib/rename/rename.ts +++ b/src/vs/editor/contrib/rename/rename.ts @@ -22,7 +22,7 @@ import { MessageController } from 'vs/editor/contrib/message/messageController'; import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/editor/browser/core/editorState'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditService, ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -226,7 +226,7 @@ class RenameController implements IEditorContribution { return; } - this._bulkEditService.apply(renameResult, { + this._bulkEditService.apply(ResourceEdit.convert(renameResult), { editor: this.editor, showPreview: inputFieldResult.wantsPreview, label: nls.localize('label', "Renaming '{0}'", loc?.text), diff --git a/src/vs/editor/contrib/rename/test/onTypeRename.test.ts b/src/vs/editor/contrib/rename/test/onTypeRename.test.ts index 86177205b90..303b3554445 100644 --- a/src/vs/editor/contrib/rename/test/onTypeRename.test.ts +++ b/src/vs/editor/contrib/rename/test/onTypeRename.test.ts @@ -6,19 +6,29 @@ import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { Position } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { Handler } from 'vs/editor/common/editorCommon'; import * as modes from 'vs/editor/common/modes'; import { OnTypeRenameContribution } from 'vs/editor/contrib/rename/onTypeRename'; import { createTestCodeEditor, ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; +import { ITextModel } from 'vs/editor/common/model'; +import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/model/wordHelper'; const mockFile = URI.parse('test:somefile.ttt'); const mockFileSelector = { scheme: 'test' }; const timeout = 30; +interface TestEditor { + setPosition(pos: Position): Promise; + setSelection(sel: IRange): Promise; + trigger(source: string | null | undefined, handlerId: string, payload: any): Promise; + undo(): void; + redo(): void; +} + suite('On type rename', () => { const disposables = new DisposableStore(); @@ -45,26 +55,54 @@ suite('On type rename', () => { function testCase( name: string, - initialState: { text: string | string[], ranges: Range[], stopPattern?: RegExp }, - operations: (editor: ITestCodeEditor, contrib: OnTypeRenameContribution) => Promise, + initialState: { text: string | string[], responseWordPattern?: RegExp, providerWordPattern?: RegExp }, + operations: (editor: TestEditor) => Promise, expectedEndText: string | string[] ) { test(name, async () => { disposables.add(modes.OnTypeRenameProviderRegistry.register(mockFileSelector, { - stopPattern: initialState.stopPattern || /^\s/, - - provideOnTypeRenameRanges() { - return initialState.ranges; + wordPattern: initialState.providerWordPattern, + provideOnTypeRenameRanges(model: ITextModel, pos: IPosition) { + const wordAtPos = model.getWordAtPosition(pos); + if (wordAtPos) { + const matches = model.findMatches(wordAtPos.word, false, false, true, USUAL_WORD_SEPARATORS, false); + assert.ok(matches.length > 0); + return { ranges: matches.map(m => m.range), wordPattern: initialState.responseWordPattern }; + } + return { ranges: [], wordPattern: initialState.responseWordPattern }; } })); const editor = createMockEditor(initialState.text); + editor.updateOptions({ renameOnType: true }); const ontypeRenameContribution = editor.registerAndInstantiateContribution( OnTypeRenameContribution.ID, OnTypeRenameContribution ); + ontypeRenameContribution.setDebounceDuration(0); - await operations(editor, ontypeRenameContribution); + const testEditor: TestEditor = { + setPosition(pos: Position) { + editor.setPosition(pos); + return ontypeRenameContribution.currentUpdateTriggerPromise; + }, + setSelection(sel: IRange) { + editor.setSelection(sel); + return ontypeRenameContribution.currentUpdateTriggerPromise; + }, + trigger(source: string | null | undefined, handlerId: string, payload: any) { + editor.trigger(source, handlerId, payload); + return ontypeRenameContribution.currentSyncTriggerPromise; + }, + undo() { + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + }, + redo() { + CoreEditingCommands.Redo.runEditorCommand(null, editor, null); + } + }; + + await operations(testEditor); return new Promise((resolve) => { setTimeout(() => { @@ -80,349 +118,322 @@ suite('On type rename', () => { } const state = { - text: '', - ranges: [ - new Range(1, 2, 1, 5), - new Range(1, 8, 1, 11), - ] + text: '' }; /** * Simple insertion */ - testCase('Simple insert - initial', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert - initial', state, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Simple insert - middle', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert - middle', state, async (editor) => { const pos = new Position(1, 3); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Simple insert - end', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert - end', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); /** * Simple insertion - end */ - testCase('Simple insert end - initial', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert end - initial', state, async (editor) => { const pos = new Position(1, 8); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Simple insert end - middle', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert end - middle', state, async (editor) => { const pos = new Position(1, 9); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Simple insert end - end', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert end - end', state, async (editor) => { const pos = new Position(1, 11); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); /** * Boundary insertion */ - testCase('Simple insert - out of boundary', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert - out of boundary', state, async (editor) => { const pos = new Position(1, 1); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, 'i'); - testCase('Simple insert - out of boundary 2', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert - out of boundary 2', state, async (editor) => { const pos = new Position(1, 6); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, 'i'); - testCase('Simple insert - out of boundary 3', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert - out of boundary 3', state, async (editor) => { const pos = new Position(1, 7); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Simple insert - out of boundary 4', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert - out of boundary 4', state, async (editor) => { const pos = new Position(1, 12); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, 'i'); /** * Insert + Move */ - testCase('Continuous insert', state, async (editor, ontypeRenameContribution) => { + testCase('Continuous insert', state, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Insert - move - insert', state, async (editor, ontypeRenameContribution) => { + testCase('Insert - move - insert', state, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); - editor.setPosition(new Position(1, 4)); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(new Position(1, 4)); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Insert - move - insert outside region', state, async (editor, ontypeRenameContribution) => { + testCase('Insert - move - insert outside region', state, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); - editor.setPosition(new Position(1, 7)); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(new Position(1, 7)); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, 'i'); /** * Selection insert */ - testCase('Selection insert - simple', state, async (editor, ontypeRenameContribution) => { + testCase('Selection insert - simple', state, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.setSelection(new Range(1, 2, 1, 3)); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.setSelection(new Range(1, 2, 1, 3)); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Selection insert - whole', state, async (editor, ontypeRenameContribution) => { + testCase('Selection insert - whole', state, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.setSelection(new Range(1, 2, 1, 5)); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.setSelection(new Range(1, 2, 1, 5)); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Selection insert - across boundary', state, async (editor, ontypeRenameContribution) => { + testCase('Selection insert - across boundary', state, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.setSelection(new Range(1, 1, 1, 3)); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.setSelection(new Range(1, 1, 1, 3)); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, 'ioo>'); /** * @todo * Undefined behavior */ - // testCase('Selection insert - across two boundary', state, async (editor, ontypeRenameContribution) => { + // testCase('Selection insert - across two boundary', state, async (editor) => { // const pos = new Position(1, 2); - // editor.setPosition(pos); - // await ontypeRenameContribution.run(pos, true); - // editor.setSelection(new Range(1, 4, 1, 9)); - // editor.trigger('keyboard', Handler.Type, { text: 'i' }); + // await editor.setPosition(pos); + // await ontypeRenameContribution.updateLinkedUI(pos); + // await editor.setSelection(new Range(1, 4, 1, 9)); + // await editor.trigger('keyboard', Handler.Type, { text: 'i' }); // }, ''); /** * Break out behavior */ - testCase('Breakout - type space', state, async (editor, ontypeRenameContribution) => { + testCase('Breakout - type space', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: ' ' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: ' ' }); }, ''); - testCase('Breakout - type space then undo', state, async (editor, ontypeRenameContribution) => { + testCase('Breakout - type space then undo', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: ' ' }); - CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: ' ' }); + editor.undo(); }, ''); - testCase('Breakout - type space in middle', state, async (editor, ontypeRenameContribution) => { + testCase('Breakout - type space in middle', state, async (editor) => { const pos = new Position(1, 4); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: ' ' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: ' ' }); }, ''); - testCase('Breakout - paste content starting with space', state, async (editor, ontypeRenameContribution) => { + testCase('Breakout - paste content starting with space', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' }); }, ''); - testCase('Breakout - paste content starting with space then undo', state, async (editor, ontypeRenameContribution) => { + testCase('Breakout - paste content starting with space then undo', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' }); - CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' }); + editor.undo(); }, ''); - testCase('Breakout - paste content starting with space in middle', state, async (editor, ontypeRenameContribution) => { + testCase('Breakout - paste content starting with space in middle', state, async (editor) => { const pos = new Position(1, 4); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Paste, { text: ' i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Paste, { text: ' i' }); }, ''); /** - * Break out with custom stopPattern + * Break out with custom provider wordPattern */ const state3 = { ...state, - stopPattern: /^s/ + providerWordPattern: /[a-yA-Y]+/ }; - testCase('Breakout with stop pattern - insert', state3, async (editor, ontypeRenameContribution) => { + testCase('Breakout with stop pattern - insert', state3, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Breakout with stop pattern - insert stop char', state3, async (editor, ontypeRenameContribution) => { + testCase('Breakout with stop pattern - insert stop char', state3, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 's' }); - }, ''); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'z' }); + }, ''); - testCase('Breakout with stop pattern - paste char', state3, async (editor, ontypeRenameContribution) => { + testCase('Breakout with stop pattern - paste char', state3, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Paste, { text: 's' }); - }, ''); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Paste, { text: 'z' }); + }, ''); - testCase('Breakout with stop pattern - paste string', state3, async (editor, ontypeRenameContribution) => { + testCase('Breakout with stop pattern - paste string', state3, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Paste, { text: 'so' }); - }, ''); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Paste, { text: 'zo' }); + }, ''); - testCase('Breakout with stop pattern - insert at end', state3, async (editor, ontypeRenameContribution) => { + testCase('Breakout with stop pattern - insert at end', state3, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 's' }); - }, ''); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'z' }); + }, ''); + + const state4 = { + ...state, + providerWordPattern: /[a-yA-Y]+/, + responseWordPattern: /[a-eA-E]+/ + }; + + testCase('Breakout with stop pattern - insert stop char, respos', state4, async (editor) => { + const pos = new Position(1, 2); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, ''); /** * Delete */ - testCase('Delete - left char', state, async (editor, ontypeRenameContribution) => { + testCase('Delete - left char', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', 'deleteLeft', {}); + await editor.setPosition(pos); + await editor.trigger('keyboard', 'deleteLeft', {}); }, ''); - testCase('Delete - left char then undo', state, async (editor, ontypeRenameContribution) => { + testCase('Delete - left char then undo', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', 'deleteLeft', {}); - CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + await editor.setPosition(pos); + await editor.trigger('keyboard', 'deleteLeft', {}); + editor.undo(); }, ''); - testCase('Delete - left word', state, async (editor, ontypeRenameContribution) => { + testCase('Delete - left word', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', 'deleteWordLeft', {}); + await editor.setPosition(pos); + await editor.trigger('keyboard', 'deleteWordLeft', {}); }, '<>'); - testCase('Delete - left word then undo', state, async (editor, ontypeRenameContribution) => { + testCase('Delete - left word then undo', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', 'deleteWordLeft', {}); - CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + await editor.setPosition(pos); + await editor.trigger('keyboard', 'deleteWordLeft', {}); + editor.undo(); + editor.undo(); }, ''); /** * Todo: Fix test */ - // testCase('Delete - left all', state, async (editor, ontypeRenameContribution) => { + // testCase('Delete - left all', state, async (editor) => { // const pos = new Position(1, 3); - // editor.setPosition(pos); - // await ontypeRenameContribution.run(pos, true); - // editor.trigger('keyboard', 'deleteAllLeft', {}); + // await editor.setPosition(pos); + // await ontypeRenameContribution.updateLinkedUI(pos); + // await editor.trigger('keyboard', 'deleteAllLeft', {}); // }, '>'); /** * Todo: Fix test */ - // testCase('Delete - left all then undo', state, async (editor, ontypeRenameContribution) => { + // testCase('Delete - left all then undo', state, async (editor) => { // const pos = new Position(1, 5); - // editor.setPosition(pos); - // await ontypeRenameContribution.run(pos, true); - // editor.trigger('keyboard', 'deleteAllLeft', {}); - // CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + // await editor.setPosition(pos); + // await ontypeRenameContribution.updateLinkedUI(pos); + // await editor.trigger('keyboard', 'deleteAllLeft', {}); + // editor.undo(); // }, '>'); - testCase('Delete - left all then undo twice', state, async (editor, ontypeRenameContribution) => { + testCase('Delete - left all then undo twice', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', 'deleteAllLeft', {}); - CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + await editor.setPosition(pos); + await editor.trigger('keyboard', 'deleteAllLeft', {}); + editor.undo(); + editor.undo(); }, ''); - testCase('Delete - selection', state, async (editor, ontypeRenameContribution) => { + testCase('Delete - selection', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.setSelection(new Range(1, 2, 1, 3)); - editor.trigger('keyboard', 'deleteLeft', {}); + await editor.setPosition(pos); + await editor.setSelection(new Range(1, 2, 1, 3)); + await editor.trigger('keyboard', 'deleteLeft', {}); }, ''); - testCase('Delete - selection across boundary', state, async (editor, ontypeRenameContribution) => { + testCase('Delete - selection across boundary', state, async (editor) => { const pos = new Position(1, 3); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.setSelection(new Range(1, 1, 1, 3)); - editor.trigger('keyboard', 'deleteLeft', {}); + await editor.setPosition(pos); + await editor.setSelection(new Range(1, 1, 1, 3)); + await editor.trigger('keyboard', 'deleteLeft', {}); }, 'oo>'); /** * Undo / redo */ - testCase('Undo/redo - simple undo', state, async (editor, ontypeRenameContribution) => { + testCase('Undo/redo - simple undo', state, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); - CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); + editor.undo(); + editor.undo(); }, ''); - testCase('Undo/redo - simple undo/redo', state, async (editor, ontypeRenameContribution) => { + testCase('Undo/redo - simple undo/redo', state, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); - CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - CoreEditingCommands.Redo.runEditorCommand(null, editor, null); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); + editor.undo(); + editor.redo(); }, ''); /** @@ -432,18 +443,13 @@ suite('On type rename', () => { text: [ '', '' - ], - ranges: [ - new Range(1, 2, 1, 5), - new Range(2, 3, 2, 6), ] }; - testCase('Multiline insert', state2, async (editor, ontypeRenameContribution) => { + testCase('Multiline insert', state2, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, [ '', '' diff --git a/src/vs/editor/contrib/snippet/snippetController2.ts b/src/vs/editor/contrib/snippet/snippetController2.ts index 8005cf5badf..7c35372a614 100644 --- a/src/vs/editor/contrib/snippet/snippetController2.ts +++ b/src/vs/editor/contrib/snippet/snippetController2.ts @@ -18,6 +18,7 @@ import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from ' import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ILogService } from 'vs/platform/log/common/log'; import { SnippetSession } from './snippetSession'; +import { OvertypingCapturer } from 'vs/editor/contrib/suggest/suggestOvertypingCapturer'; export interface ISnippetInsertOptions { overwriteBefore: number; @@ -26,6 +27,7 @@ export interface ISnippetInsertOptions { undoStopBefore: boolean; undoStopAfter: boolean; clipboardText: string | undefined; + overtypingCapturer: OvertypingCapturer | undefined; } const _defaultOptions: ISnippetInsertOptions = { @@ -34,7 +36,8 @@ const _defaultOptions: ISnippetInsertOptions = { undoStopBefore: true, undoStopAfter: true, adjustWhitespace: true, - clipboardText: undefined + clipboardText: undefined, + overtypingCapturer: undefined }; export class SnippetController2 implements IEditorContribution { diff --git a/src/vs/editor/contrib/snippet/snippetSession.ts b/src/vs/editor/contrib/snippet/snippetSession.ts index 717b223cb4a..7f91f835c4c 100644 --- a/src/vs/editor/contrib/snippet/snippetSession.ts +++ b/src/vs/editor/contrib/snippet/snippetSession.ts @@ -23,6 +23,7 @@ import * as colors from 'vs/platform/theme/common/colorRegistry'; import { withNullAsUndefined } from 'vs/base/common/types'; import { ILabelService } from 'vs/platform/label/common/label'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { OvertypingCapturer } from 'vs/editor/contrib/suggest/suggestOvertypingCapturer'; registerThemingParticipant((theme, collector) => { @@ -319,13 +320,15 @@ export interface ISnippetSessionInsertOptions { overwriteAfter: number; adjustWhitespace: boolean; clipboardText: string | undefined; + overtypingCapturer: OvertypingCapturer | undefined; } const _defaultOptions: ISnippetSessionInsertOptions = { overwriteBefore: 0, overwriteAfter: 0, adjustWhitespace: true, - clipboardText: undefined + clipboardText: undefined, + overtypingCapturer: undefined }; export class SnippetSession { @@ -382,7 +385,7 @@ export class SnippetSession { return selection; } - static createEditsAndSnippets(editor: IActiveCodeEditor, template: string, overwriteBefore: number, overwriteAfter: number, enforceFinalTabstop: boolean, adjustWhitespace: boolean, clipboardText: string | undefined): { edits: IIdentifiedSingleEditOperation[], snippets: OneSnippet[] } { + static createEditsAndSnippets(editor: IActiveCodeEditor, template: string, overwriteBefore: number, overwriteAfter: number, enforceFinalTabstop: boolean, adjustWhitespace: boolean, clipboardText: string | undefined, overtypingCapturer: OvertypingCapturer | undefined): { edits: IIdentifiedSingleEditOperation[], snippets: OneSnippet[] } { const edits: IIdentifiedSingleEditOperation[] = []; const snippets: OneSnippet[] = []; @@ -449,7 +452,7 @@ export class SnippetSession { snippet.resolveVariables(new CompositeSnippetVariableResolver([ modelBasedVariableResolver, new ClipboardBasedVariableResolver(readClipboardText, idx, indexedSelections.length, editor.getOption(EditorOption.multiCursorPaste) === 'spread'), - new SelectionBasedVariableResolver(model, selection), + new SelectionBasedVariableResolver(model, selection, idx, overtypingCapturer), new CommentBasedVariableResolver(model, selection), new TimeBasedVariableResolver, new WorkspaceBasedVariableResolver(workspaceService), @@ -496,7 +499,7 @@ export class SnippetSession { } // make insert edit and start with first selections - const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, this._template, this._options.overwriteBefore, this._options.overwriteAfter, false, this._options.adjustWhitespace, this._options.clipboardText); + const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, this._template, this._options.overwriteBefore, this._options.overwriteAfter, false, this._options.adjustWhitespace, this._options.clipboardText, this._options.overtypingCapturer); this._snippets = snippets; this._editor.executeEdits('snippet', edits, undoEdits => { @@ -516,7 +519,7 @@ export class SnippetSession { return; } this._templateMerges.push([this._snippets[0]._nestingLevel, this._snippets[0]._placeholderGroupsIdx, template]); - const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, template, options.overwriteBefore, options.overwriteAfter, true, options.adjustWhitespace, options.clipboardText); + const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, template, options.overwriteBefore, options.overwriteAfter, true, options.adjustWhitespace, options.clipboardText, options.overtypingCapturer); this._editor.executeEdits('snippet', edits, undoEdits => { for (const snippet of this._snippets) { diff --git a/src/vs/editor/contrib/snippet/snippetVariables.ts b/src/vs/editor/contrib/snippet/snippetVariables.ts index 3253898bdee..cd5d505bcc9 100644 --- a/src/vs/editor/contrib/snippet/snippetVariables.ts +++ b/src/vs/editor/contrib/snippet/snippetVariables.ts @@ -16,6 +16,7 @@ import { isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier, WORKSPACE_EXT import { ILabelService } from 'vs/platform/label/common/label'; import { normalizeDriveLetter } from 'vs/base/common/labels'; import { URI } from 'vs/base/common/uri'; +import { OvertypingCapturer } from 'vs/editor/contrib/suggest/suggestOvertypingCapturer'; export const KnownSnippetVariableNames: { [key: string]: true } = Object.freeze({ 'CURRENT_YEAR': true, @@ -71,7 +72,9 @@ export class SelectionBasedVariableResolver implements VariableResolver { constructor( private readonly _model: ITextModel, - private readonly _selection: Selection + private readonly _selection: Selection, + private readonly _selectionIdx: number, + private readonly _overtypingCapturer: OvertypingCapturer | undefined ) { // } @@ -82,7 +85,18 @@ export class SelectionBasedVariableResolver implements VariableResolver { if (name === 'SELECTION' || name === 'TM_SELECTED_TEXT') { let value = this._model.getValueInRange(this._selection) || undefined; - if (value && this._selection.startLineNumber !== this._selection.endLineNumber && variable.snippet) { + let isMultiline = this._selection.startLineNumber !== this._selection.endLineNumber; + + // If there was no selected text, try to get last overtyped text + if (!value && this._overtypingCapturer) { + const info = this._overtypingCapturer.getLastOvertypedInfo(this._selectionIdx); + if (info) { + value = info.value; + isMultiline = info.multiline; + } + } + + if (value && isMultiline && variable.snippet) { // Selection is a multiline string which we indentation we now // need to adjust. We compare the indentation of this variable // with the indentation at the editor position and add potential diff --git a/src/vs/editor/contrib/snippet/test/snippetSession.test.ts b/src/vs/editor/contrib/snippet/test/snippetSession.test.ts index cedf6be949e..f72727ccdce 100644 --- a/src/vs/editor/contrib/snippet/test/snippetSession.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetSession.test.ts @@ -127,7 +127,7 @@ suite('SnippetSession', function () { test('snippets, newline NO whitespace adjust', () => { editor.setSelection(new Selection(2, 5, 2, 5)); - const session = new SnippetSession(editor, 'abc\n foo\n bar\n$0', { overwriteBefore: 0, overwriteAfter: 0, adjustWhitespace: false, clipboardText: undefined }); + const session = new SnippetSession(editor, 'abc\n foo\n bar\n$0', { overwriteBefore: 0, overwriteAfter: 0, adjustWhitespace: false, clipboardText: undefined, overtypingCapturer: undefined }); session.insert(); assert.equal(editor.getModel()!.getValue(), 'function foo() {\n abc\n foo\n bar\nconsole.log(a);\n}'); }); @@ -649,7 +649,7 @@ suite('SnippetSession', function () { assert.ok(actual.equalsSelection(new Selection(1, 9, 1, 12))); editor.setSelections([new Selection(1, 9, 1, 12)]); - new SnippetSession(editor, 'far', { overwriteBefore: 3, overwriteAfter: 0, adjustWhitespace: true, clipboardText: undefined }).insert(); + new SnippetSession(editor, 'far', { overwriteBefore: 3, overwriteAfter: 0, adjustWhitespace: true, clipboardText: undefined, overtypingCapturer: undefined }).insert(); assert.equal(model.getValue(), 'console.far'); }); }); diff --git a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts index bb95bdfcadf..658039f8065 100644 --- a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts @@ -11,7 +11,7 @@ import { SnippetParser, Variable, VariableResolver } from 'vs/editor/contrib/sni import { TextModel } from 'vs/editor/common/model/textModel'; import { Workspace, toWorkspaceFolders, IWorkspace, IWorkspaceContextService, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ILabelService } from 'vs/platform/label/common/label'; -import { mock } from 'vs/editor/contrib/suggest/test/suggestModel.test'; +import { mock } from 'vs/base/test/common/mock'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; suite('Snippet Variables Resolver', function () { @@ -34,7 +34,7 @@ suite('Snippet Variables Resolver', function () { resolver = new CompositeSnippetVariableResolver([ new ModelBasedVariableResolver(labelService, model), - new SelectionBasedVariableResolver(model, new Selection(1, 1, 1, 1)), + new SelectionBasedVariableResolver(model, new Selection(1, 1, 1, 1), 0, undefined), ]); }); @@ -102,24 +102,24 @@ suite('Snippet Variables Resolver', function () { test('editor variables, selection', function () { - resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 2, 3)); + resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 2, 3), 0, undefined); assertVariableResolve(resolver, 'TM_SELECTED_TEXT', 'his is line one\nth'); assertVariableResolve(resolver, 'TM_CURRENT_LINE', 'this is line two'); assertVariableResolve(resolver, 'TM_LINE_INDEX', '1'); assertVariableResolve(resolver, 'TM_LINE_NUMBER', '2'); - resolver = new SelectionBasedVariableResolver(model, new Selection(2, 3, 1, 2)); + resolver = new SelectionBasedVariableResolver(model, new Selection(2, 3, 1, 2), 0, undefined); assertVariableResolve(resolver, 'TM_SELECTED_TEXT', 'his is line one\nth'); assertVariableResolve(resolver, 'TM_CURRENT_LINE', 'this is line one'); assertVariableResolve(resolver, 'TM_LINE_INDEX', '0'); assertVariableResolve(resolver, 'TM_LINE_NUMBER', '1'); - resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 1, 2)); + resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 1, 2), 0, undefined); assertVariableResolve(resolver, 'TM_SELECTED_TEXT', undefined); assertVariableResolve(resolver, 'TM_CURRENT_WORD', 'this'); - resolver = new SelectionBasedVariableResolver(model, new Selection(3, 1, 3, 1)); + resolver = new SelectionBasedVariableResolver(model, new Selection(3, 1, 3, 1), 0, undefined); assertVariableResolve(resolver, 'TM_CURRENT_WORD', undefined); }); diff --git a/src/vs/editor/contrib/suggest/suggestController.ts b/src/vs/editor/contrib/suggest/suggestController.ts index 60dd04f3d72..3b92a21d7cc 100644 --- a/src/vs/editor/contrib/suggest/suggestController.ts +++ b/src/vs/editor/contrib/suggest/suggestController.ts @@ -34,6 +34,7 @@ import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerServ import { IdleValue } from 'vs/base/common/async'; import { isObject, assertType } from 'vs/base/common/types'; import { CommitCharacterController } from './suggestCommitCharacters'; +import { OvertypingCapturer } from './suggestOvertypingCapturer'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { TrackedRangeStickiness, ITextModel } from 'vs/editor/common/model'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -112,6 +113,7 @@ export class SuggestController implements IEditorContribution { private readonly _alternatives: IdleValue; private readonly _lineSuffix = new MutableDisposable(); private readonly _toDispose = new DisposableStore(); + private readonly _overtypingCapturer: IdleValue; constructor( editor: ICodeEditor, @@ -203,6 +205,11 @@ export class SuggestController implements IEditorContribution { return widget; })); + // Wire up text overtyping capture + this._overtypingCapturer = this._toDispose.add(new IdleValue(() => { + return this._toDispose.add(new OvertypingCapturer(this.editor, this.model)); + })); + this._alternatives = this._toDispose.add(new IdleValue(() => { return this._toDispose.add(new SuggestAlternatives(this.editor, this._contextKeyService)); })); @@ -361,7 +368,8 @@ export class SuggestController implements IEditorContribution { undoStopBefore: false, undoStopAfter: false, adjustWhitespace: !(item.completion.insertTextRules! & CompletionItemInsertTextRule.KeepWhitespace), - clipboardText: event.model.clipboardText + clipboardText: event.model.clipboardText, + overtypingCapturer: this._overtypingCapturer.value }); if (!(flags & InsertFlags.NoAfterUndoStop)) { diff --git a/src/vs/editor/contrib/suggest/suggestModel.ts b/src/vs/editor/contrib/suggest/suggestModel.ts index d3daa598ebb..bb4f269ece8 100644 --- a/src/vs/editor/contrib/suggest/suggestModel.ts +++ b/src/vs/editor/contrib/suggest/suggestModel.ts @@ -229,7 +229,7 @@ export class SuggestModel implements IDisposable { if (supports) { // keep existing items that where not computed by the // supports/providers that want to trigger now - const items: CompletionItem[] | undefined = this._completionModel ? this._completionModel.adopt(supports) : undefined; + const items = this._completionModel?.adopt(supports); this.trigger({ auto: true, shy: false, triggerCharacter: lastChar }, Boolean(this._completionModel), supports, items); } }; @@ -556,6 +556,12 @@ export class SuggestModel implements IDisposable { return; } + if (ctx.leadingWord.word.length !== 0 && ctx.leadingWord.startColumn > this._context.leadingWord.startColumn) { + // started a new word while IntelliSense shows -> retrigger + this.trigger({ auto: this._context.auto, shy: false }, true); + return; + } + if (ctx.column > this._context.column && this._completionModel.incomplete.size > 0 && ctx.leadingWord.word.length !== 0) { // typed -> moved cursor RIGHT & incomple model & still on a word -> retrigger const { incomplete } = this._completionModel; diff --git a/src/vs/editor/contrib/suggest/suggestOvertypingCapturer.ts b/src/vs/editor/contrib/suggest/suggestOvertypingCapturer.ts new file mode 100644 index 00000000000..44eec0ca874 --- /dev/null +++ b/src/vs/editor/contrib/suggest/suggestOvertypingCapturer.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 { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { SuggestModel } from 'vs/editor/contrib/suggest/suggestModel'; + +export class OvertypingCapturer implements IDisposable { + + private static readonly _maxSelectionLength = 51200; + private readonly _disposables = new DisposableStore(); + + private _lastOvertyped: { value: string; multiline: boolean }[] = []; + private _empty: boolean = true; + + constructor(editor: ICodeEditor, suggestModel: SuggestModel) { + + this._disposables.add(editor.onWillType(() => { + if (!this._empty) { + return; + } + if (!editor.hasModel()) { + return; + } + + const selections = editor.getSelections(); + const selectionsLength = selections.length; + + // Check if it will overtype any selections + let willOvertype = false; + for (let i = 0; i < selectionsLength; i++) { + if (!selections[i].isEmpty()) { + willOvertype = true; + break; + } + } + if (!willOvertype) { + return; + } + + this._lastOvertyped = []; + const model = editor.getModel(); + for (let i = 0; i < selectionsLength; i++) { + const selection = selections[i]; + // Check for overtyping capturer restrictions + if (model.getValueLengthInRange(selection) > OvertypingCapturer._maxSelectionLength) { + return; + } + this._lastOvertyped[i] = { value: model.getValueInRange(selection), multiline: selection.startLineNumber !== selection.endLineNumber }; + } + this._empty = false; + })); + + this._disposables.add(suggestModel.onDidCancel(e => { + if (!this._empty) { + this._empty = true; + } + })); + } + + getLastOvertypedInfo(idx: number): { value: string; multiline: boolean } | undefined { + if (!this._empty && idx >= 0 && idx < this._lastOvertyped.length) { + return this._lastOvertyped[idx]; + } + return undefined; + } + + dispose() { + this._disposables.dispose(); + } +} diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 7b19fefb903..107bee50e37 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -373,7 +373,7 @@ class SuggestionDetails { this.docs.textContent = documentation; } else { this.docs.classList.add('markdown-docs'); - this.docs.innerHTML = ''; + this.docs.innerText = ''; const renderedContents = this.markdownRenderer.render(documentation); this.renderDisposeable = renderedContents; this.docs.appendChild(renderedContents.element); diff --git a/src/vs/editor/contrib/suggest/test/suggestController.test.ts b/src/vs/editor/contrib/suggest/test/suggestController.test.ts index ffeccaf1ba3..8644d4498bb 100644 --- a/src/vs/editor/contrib/suggest/test/suggestController.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestController.test.ts @@ -17,7 +17,7 @@ import { ISuggestMemoryService } from 'vs/editor/contrib/suggest/suggestMemory'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; -import { mock } from 'vs/editor/contrib/suggest/test/suggestModel.test'; +import { mock } from 'vs/base/test/common/mock'; import { Selection } from 'vs/editor/common/core/selection'; import { CompletionProviderRegistry, CompletionItemKind, CompletionItemInsertTextRule } from 'vs/editor/common/modes'; import { Event } from 'vs/base/common/event'; diff --git a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts index edf5997475f..360ce3a79d1 100644 --- a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts @@ -34,14 +34,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; - -export interface Ctor { - new(): T; -} - -export function mock(): Ctor { - return function () { } as any; -} +import { mock } from 'vs/base/test/common/mock'; function createMockEditor(model: TextModel): ITestCodeEditor { @@ -798,4 +791,68 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { }); }); + + + test('Trigger (full) completions when (incomplete) completions are already active #99504', function () { + + let countA = 0; + let countB = 0; + + disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, { + provideCompletionItems(doc, pos) { + countA += 1; + return { + incomplete: false, // doesn't matter if incomplete or not + suggestions: [{ + kind: CompletionItemKind.Class, + label: 'Z aaa', + insertText: 'Z aaa', + range: new Range(1, 1, pos.lineNumber, pos.column) + }], + }; + } + })); + disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, { + provideCompletionItems(doc, pos) { + countB += 1; + return { + incomplete: false, + suggestions: [{ + kind: CompletionItemKind.Folder, + label: 'aaa', + insertText: 'aaa', + range: getDefaultSuggestRange(doc, pos) + }], + }; + }, + })); + + return withOracle(async (model, editor) => { + + await assertEvent(model.onDidSuggest, () => { + editor.setValue(''); + editor.setSelection(new Selection(1, 1, 1, 1)); + editor.trigger('keyboard', Handler.Type, { text: 'Z' }); + + }, event => { + assert.equal(event.auto, true); + assert.equal(event.completionModel.items.length, 1); + assert.equal(event.completionModel.items[0].textLabel, 'Z aaa'); + }); + + await assertEvent(model.onDidSuggest, () => { + // started another word: Z a| + // item should be: Z aaa, aaa + editor.trigger('keyboard', Handler.Type, { text: ' a' }); + }, event => { + assert.equal(event.auto, true); + assert.equal(event.completionModel.items.length, 2); + assert.equal(event.completionModel.items[0].textLabel, 'Z aaa'); + assert.equal(event.completionModel.items[1].textLabel, 'aaa'); + + assert.equal(countA, 2); // should we keep the suggestions from the "active" provider? + assert.equal(countB, 2); + }); + }); + }); }); diff --git a/src/vs/editor/contrib/suggest/test/wordDistance.test.ts b/src/vs/editor/contrib/suggest/test/wordDistance.test.ts index ede0459b22c..05ccef17d53 100644 --- a/src/vs/editor/contrib/suggest/test/wordDistance.test.ts +++ b/src/vs/editor/contrib/suggest/test/wordDistance.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { EditorSimpleWorker } from 'vs/editor/common/services/editorSimpleWorker'; -import { mock } from 'vs/editor/contrib/suggest/test/suggestModel.test'; +import { mock } from 'vs/base/test/common/mock'; import { EditorWorkerHost, EditorWorkerServiceImpl } from 'vs/editor/common/services/editorWorkerServiceImpl'; import { IModelService } from 'vs/editor/common/services/modelService'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; @@ -81,11 +81,16 @@ suite('suggest, word distance', function () { distance = await WordDistance.create(service, editor); + disposables.add(service); disposables.add(mode); disposables.add(model); disposables.add(editor); }); + teardown(function () { + disposables.clear(); + }); + function createSuggestItem(label: string, overwriteBefore: number, position: IPosition): CompletionItem { const suggestion: modes.CompletionItem = { label, diff --git a/src/vs/editor/contrib/wordOperations/wordOperations.ts b/src/vs/editor/contrib/wordOperations/wordOperations.ts index 791e831aab0..2e2b7013238 100644 --- a/src/vs/editor/contrib/wordOperations/wordOperations.ts +++ b/src/vs/editor/contrib/wordOperations/wordOperations.ts @@ -101,13 +101,7 @@ export class CursorWordStartLeft extends WordLeftCommand { inSelectionMode: false, wordNavigationType: WordNavigationType.WordStart, id: 'cursorWordStartLeft', - precondition: undefined, - kbOpts: { - kbExpr: EditorContextKeys.textInputFocus, - primary: KeyMod.CtrlCmd | KeyCode.LeftArrow, - mac: { primary: KeyMod.Alt | KeyCode.LeftArrow }, - weight: KeybindingWeight.EditorContrib - } + precondition: undefined }); } } @@ -129,7 +123,13 @@ export class CursorWordLeft extends WordLeftCommand { inSelectionMode: false, wordNavigationType: WordNavigationType.WordStartFast, id: 'cursorWordLeft', - precondition: undefined + precondition: undefined, + kbOpts: { + kbExpr: EditorContextKeys.textInputFocus, + primary: KeyMod.CtrlCmd | KeyCode.LeftArrow, + mac: { primary: KeyMod.Alt | KeyCode.LeftArrow }, + weight: KeybindingWeight.EditorContrib + } }); } } @@ -140,13 +140,7 @@ export class CursorWordStartLeftSelect extends WordLeftCommand { inSelectionMode: true, wordNavigationType: WordNavigationType.WordStart, id: 'cursorWordStartLeftSelect', - precondition: undefined, - kbOpts: { - kbExpr: EditorContextKeys.textInputFocus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.LeftArrow, - mac: { primary: KeyMod.Alt | KeyMod.Shift | KeyCode.LeftArrow }, - weight: KeybindingWeight.EditorContrib - } + precondition: undefined }); } } @@ -168,7 +162,13 @@ export class CursorWordLeftSelect extends WordLeftCommand { inSelectionMode: true, wordNavigationType: WordNavigationType.WordStartFast, id: 'cursorWordLeftSelect', - precondition: undefined + precondition: undefined, + kbOpts: { + kbExpr: EditorContextKeys.textInputFocus, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.LeftArrow, + mac: { primary: KeyMod.Alt | KeyMod.Shift | KeyCode.LeftArrow }, + weight: KeybindingWeight.EditorContrib + } }); } } diff --git a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts index 08d629e4260..31108f99ddb 100644 --- a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts +++ b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./inspectTokens'; +import { $, append, reset } from 'vs/base/browser/dom'; import { CharCode } from 'vs/base/common/charCode'; import { Color } from 'vs/base/common/color'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; -import { escape } from 'vs/base/common/strings'; import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { Position } from 'vs/editor/common/core/position'; @@ -115,23 +115,11 @@ function renderTokenText(tokenText: string): string { let charCode = tokenText.charCodeAt(charIndex); switch (charCode) { case CharCode.Tab: - result += '→'; + result += '\u2192'; // → break; case CharCode.Space: - result += '·'; - break; - - case CharCode.LessThan: - result += '<'; - break; - - case CharCode.GreaterThan: - result += '>'; - break; - - case CharCode.Ampersand: - result += '&'; + result += '\u00B7'; // · break; default: @@ -211,8 +199,6 @@ class InspectTokensWidget extends Disposable implements IContentWidget { } } - let result = ''; - let lineContent = this._model.getLineContent(position.lineNumber); let tokenText = ''; if (token1Index < data.tokens1.length) { @@ -220,26 +206,43 @@ class InspectTokensWidget extends Disposable implements IContentWidget { let tokenEndIndex = token1Index + 1 < data.tokens1.length ? data.tokens1[token1Index + 1].offset : lineContent.length; tokenText = lineContent.substring(tokenStartIndex, tokenEndIndex); } - result += `

    ${renderTokenText(tokenText)}(${tokenText.length} ${tokenText.length === 1 ? 'char' : 'chars'})

    `; + reset(this._domNode, + $('h2.tm-token', undefined, renderTokenText(tokenText), + $('span.tm-token-length', undefined, `${tokenText.length} ${tokenText.length === 1 ? 'char' : 'chars'}`))); - result += `
    `; + append(this._domNode, $('hr.tokens-inspect-separator', { 'style': 'clear:both' })); - let metadata = (token2Index << 1) + 1 < data.tokens2.length ? this._decodeMetadata(data.tokens2[(token2Index << 1) + 1]) : null; - result += ``; - result += ``; - result += ``; - result += ``; - result += ``; - result += ``; - result += ``; - - result += `
    `; + const metadata = (token2Index << 1) + 1 < data.tokens2.length ? this._decodeMetadata(data.tokens2[(token2Index << 1) + 1]) : null; + append(this._domNode, $('table.tm-metadata-table', undefined, + $('tbody', undefined, + $('tr', undefined, + $('td.tm-metadata-key', undefined, 'language'), + $('td.tm-metadata-value', undefined, `${metadata ? metadata.languageIdentifier.language : '-?-'}`) + ), + $('tr', undefined, + $('td.tm-metadata-key', undefined, 'token type' as string), + $('td.tm-metadata-value', undefined, `${metadata ? this._tokenTypeToString(metadata.tokenType) : '-?-'}`) + ), + $('tr', undefined, + $('td.tm-metadata-key', undefined, 'font style' as string), + $('td.tm-metadata-value', undefined, `${metadata ? this._fontStyleToString(metadata.fontStyle) : '-?-'}`) + ), + $('tr', undefined, + $('td.tm-metadata-key', undefined, 'foreground'), + $('td.tm-metadata-value', undefined, `${metadata ? Color.Format.CSS.formatHex(metadata.foreground) : '-?-'}`) + ), + $('tr', undefined, + $('td.tm-metadata-key', undefined, 'background'), + $('td.tm-metadata-value', undefined, `${metadata ? Color.Format.CSS.formatHex(metadata.background) : '-?-'}`) + ) + ) + )); + append(this._domNode, $('hr.tokens-inspect-separator')); if (token1Index < data.tokens1.length) { - result += `${escape(data.tokens1[token1Index].type)}`; + append(this._domNode, $('span.tm-token-type', undefined, data.tokens1[token1Index].type)); } - this._domNode.innerHTML = result; this._editor.layoutContentWidget(this); } @@ -265,8 +268,8 @@ class InspectTokensWidget extends Disposable implements IContentWidget { case StandardTokenType.Comment: return 'Comment'; case StandardTokenType.String: return 'String'; case StandardTokenType.RegEx: return 'RegEx'; + default: return '??'; } - return '??'; } private _fontStyleToString(fontStyle: FontStyle): string { diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index 9051fb2cc78..b336adc20cb 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -13,14 +13,13 @@ import { OS, isLinux, isMacintosh } from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor, IDiffEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IBulkEditOptions, IBulkEditResult, IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditOptions, IBulkEditResult, IBulkEditService, ResourceEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { isDiffEditorConfigurationKey, isEditorConfigurationKey } from 'vs/editor/common/config/commonEditorConfig'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { IPosition, Position as Pos } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IEditor } from 'vs/editor/common/editorCommon'; -import { ITextModel, ITextSnapshot } from 'vs/editor/common/model'; -import { TextEdit, WorkspaceEdit, WorkspaceTextEdit } from 'vs/editor/common/modes'; +import { IIdentifiedSingleEditOperation, ITextModel, ITextSnapshot } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IResolvedTextEditorModel, ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; import { ITextResourceConfigurationService, ITextResourcePropertiesService, ITextResourceConfigurationChangeEvent } from 'vs/editor/common/services/textResourceConfigurationService'; @@ -47,6 +46,7 @@ import { SimpleServicesNLS } from 'vs/editor/common/standaloneStrings'; import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings'; import { basename } from 'vs/base/common/resources'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { NullLogService } from 'vs/platform/log/common/log'; export class SimpleModel implements IResolvedTextEditorModel { @@ -293,7 +293,7 @@ export class StandaloneKeybindingService extends AbstractKeybindingService { notificationService: INotificationService, domNode: HTMLElement ) { - super(contextKeyService, commandService, telemetryService, notificationService); + super(contextKeyService, commandService, telemetryService, notificationService, new NullLogService()); this._cachedResolver = null; this._dynamicKeybindings = []; @@ -665,42 +665,43 @@ export class SimpleBulkEditService implements IBulkEditService { return Disposable.None; } - apply(workspaceEdit: WorkspaceEdit, options?: IBulkEditOptions): Promise { + async apply(edits: ResourceEdit[], _options?: IBulkEditOptions): Promise { - let edits = new Map(); + const textEdits = new Map(); - if (workspaceEdit.edits) { - for (let edit of workspaceEdit.edits) { - if (!WorkspaceTextEdit.is(edit)) { - return Promise.reject(new Error('bad edit - only text edits are supported')); - } - let model = this._modelService.getModel(edit.resource); - if (!model) { - return Promise.reject(new Error('bad edit - model not found')); - } - let array = edits.get(model); - if (!array) { - array = []; - edits.set(model, array); - } - array.push(edit.edit); + for (let edit of edits) { + if (!(edit instanceof ResourceTextEdit)) { + throw new Error('bad edit - only text edits are supported'); } + const model = this._modelService.getModel(edit.resource); + if (!model) { + throw new Error('bad edit - model not found'); + } + if (typeof edit.versionId === 'number' && model.getVersionId() !== edit.versionId) { + throw new Error('bad state - model changed in the meantime'); + } + let array = textEdits.get(model); + if (!array) { + array = []; + textEdits.set(model, array); + } + array.push(EditOperation.replaceMove(Range.lift(edit.textEdit.range), edit.textEdit.text)); } + let totalEdits = 0; let totalFiles = 0; - edits.forEach((edits, model) => { + for (const [model, edits] of textEdits) { model.pushStackElement(); - model.pushEditOperations([], edits.map((e) => EditOperation.replaceMove(Range.lift(e.range), e.text)), () => []); + model.pushEditOperations([], edits, () => []); model.pushStackElement(); totalFiles += 1; totalEdits += edits.length; - }); + } - return Promise.resolve({ - selection: undefined, + return { ariaSummary: strings.format(SimpleServicesNLS.bulkEditServiceSummary, totalEdits, totalFiles) - }); + }; } } diff --git a/src/vs/editor/test/browser/controller/imeTester.ts b/src/vs/editor/test/browser/controller/imeTester.ts index 1668233bf9d..4a3f4e196df 100644 --- a/src/vs/editor/test/browser/controller/imeTester.ts +++ b/src/vs/editor/test/browser/controller/imeTester.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as browser from 'vs/base/browser/browser'; import { createFastDomNode } from 'vs/base/browser/fastDomNode'; import { ITextAreaInputHost, TextAreaInput } from 'vs/editor/browser/controller/textAreaInput'; import { ISimpleModel, PagedScreenReaderStrategy, TextAreaState } from 'vs/editor/browser/controller/textAreaState'; @@ -74,7 +73,7 @@ function doCreateTest(description: string, inputStr: string, expectedStr: string container.appendChild(title); let startBtn = document.createElement('button'); - startBtn.innerHTML = 'Start'; + startBtn.innerText = 'Start'; container.appendChild(startBtn); @@ -96,12 +95,6 @@ function doCreateTest(description: string, inputStr: string, expectedStr: string }; }, getScreenReaderContent: (currentState: TextAreaState): TextAreaState => { - - if (browser.isIPad) { - // Do not place anything in the textarea for the iPad - return TextAreaState.EMPTY; - } - const selection = new Range(1, 1 + cursorOffset, 1, 1 + cursorOffset + cursorLength); return PagedScreenReaderStrategy.fromEditorSelection(currentState, model, selection, 10, true); @@ -141,10 +134,10 @@ function doCreateTest(description: string, inputStr: string, expectedStr: string let expected = 'some ' + expectedStr + ' text'; if (text === expected) { - check.innerHTML = '[GOOD]'; + check.innerText = '[GOOD]'; check.className = 'check good'; } else { - check.innerHTML = '[BAD]'; + check.innerText = '[BAD]'; check.className = 'check bad'; } check.innerHTML += expected; diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 1147b17c205..0e0dc7d3d63 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -869,7 +869,7 @@ suite('viewLineRenderer.renderLine', () => { suite('viewLineRenderer.renderLine 2', () => { - function testCreateLineParts(fontIsMonospace: boolean, lineContent: string, tokens: ViewLineToken[], fauxIndentLength: number, renderWhitespace: 'none' | 'boundary' | 'selection' | 'all', selections: LineRange[] | null, expected: string): void { + function testCreateLineParts(fontIsMonospace: boolean, lineContent: string, tokens: ViewLineToken[], fauxIndentLength: number, renderWhitespace: 'none' | 'boundary' | 'selection' | 'trailing' | 'all', selections: LineRange[] | null, expected: string): void { let actual = renderViewLine(new RenderLineInput( fontIsMonospace, true, @@ -1355,6 +1355,95 @@ suite('viewLineRenderer.renderLine 2', () => { ); }); + test('createLineParts render whitespace for trailing with leading, inner, and without trailing whitespace', () => { + testCreateLineParts( + false, + ' Hello world!', + [ + createPart(4, 0), + createPart(6, 1), + createPart(14, 2) + ], + 0, + 'trailing', + null, + [ + '', + '\u00a0Hel', + 'lo', + '\u00a0world!', + '', + ].join('') + ); + }); + + test('createLineParts render whitespace for trailing with leading, inner, and trailing whitespace', () => { + testCreateLineParts( + false, + ' Hello world! \t', + [ + createPart(4, 0), + createPart(6, 1), + createPart(15, 2) + ], + 0, + 'trailing', + null, + [ + '', + '\u00a0Hel', + 'lo', + '\u00a0world!', + '\u00b7\u2192\u00a0', + '', + ].join('') + ); + }); + + test('createLineParts render whitespace for trailing with 8 leading and 8 trailing whitespaces', () => { + testCreateLineParts( + false, + ' Hello world! ', + [ + createPart(8, 1), + createPart(10, 2), + createPart(28, 3) + ], + 0, + 'trailing', + null, + [ + '', + '\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0', + 'He', + 'llo\u00a0world!', + '\u00b7\u00b7\u00b7\u00b7', + '\u00b7\u00b7\u00b7\u00b7', + '', + ].join('') + ); + }); + + test('createLineParts render whitespace for trailing with line containing only whitespaces', () => { + testCreateLineParts( + false, + ' \t ', + [ + createPart(2, 0), + createPart(3, 1), + ], + 0, + 'trailing', + null, + [ + '', + '\u00b7\u2192\u00a0\u00a0', + '\u00b7', + '', + ].join('') + ); + }); + test('createLineParts can handle unsorted inline decorations', () => { let actual = renderViewLine(new RenderLineInput( false, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 0593f65eb4a..3038f9b40f1 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1761,7 +1761,7 @@ declare namespace monaco.editor { /** * Search the model. * @param searchString The string used to search. If it is a regular expression, set `isRegex` to true. - * @param searchScope Limit the searching to only search inside this range. + * @param searchScope Limit the searching to only search inside these ranges. * @param isRegex Used to indicate that `searchString` is a regular expression. * @param matchCase Force the matching to match lower/upper case exactly. * @param wordSeparators Force the matching to match entire words only. Pass null otherwise. @@ -1769,7 +1769,7 @@ declare namespace monaco.editor { * @param limitResultCount Limit the number of results * @return The ranges where the matches are. It is empty if no matches have been found. */ - findMatches(searchString: string, searchScope: IRange, isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean, limitResultCount?: number): FindMatch[]; + findMatches(searchString: string, searchScope: IRange | IRange[], isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean, limitResultCount?: number): FindMatch[]; /** * Search the model for the next match. Loops to the beginning of the model if needed. * @param searchString The string used to search. If it is a regular expression, set `isRegex` to true. @@ -3068,7 +3068,7 @@ declare namespace monaco.editor { * Enable rendering of whitespace. * Defaults to none. */ - renderWhitespace?: 'none' | 'boundary' | 'selection' | 'all'; + renderWhitespace?: 'none' | 'boundary' | 'selection' | 'trailing' | 'all'; /** * Enable rendering of control characters. * Defaults to false. @@ -3286,6 +3286,10 @@ declare namespace monaco.editor { * Configuration options for editor find widget */ export interface IEditorFindOptions { + /** + * Controls whether the cursor should move to find matches while typing. + */ + cursorMoveOnType?: boolean; /** * Controls if we seed search string in the Find Widget with editor selection. */ @@ -4048,7 +4052,7 @@ declare namespace monaco.editor { renderLineHighlight: IEditorOption; renderLineHighlightOnlyWhenFocus: IEditorOption; renderValidationDecorations: IEditorOption; - renderWhitespace: IEditorOption; + renderWhitespace: IEditorOption; revealHorizontalRightPadding: IEditorOption; roundedSelection: IEditorOption; rulers: IEditorOption; @@ -4410,6 +4414,14 @@ declare namespace monaco.editor { overflowWidgetsDomNode?: HTMLElement; } + export interface IDiffEditorConstructionOptions extends IDiffEditorOptions { + /** + * Place overflow widgets inside an external DOM node. + * Defaults to an internal DOM node. + */ + overflowWidgetsDomNode?: HTMLElement; + } + /** * A rich code editor. */ @@ -5789,11 +5801,14 @@ declare namespace monaco.languages { * the live-rename feature. */ export interface OnTypeRenameProvider { - stopPattern?: RegExp; + wordPattern?: RegExp; /** * Provide a list of ranges that can be live-renamed together. */ - provideOnTypeRenameRanges(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult; + provideOnTypeRenameRanges(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult<{ + ranges: IRange[]; + wordPattern?: RegExp; + }>; } /** diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index d0867eb9d8d..48b171e1b76 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -110,11 +110,11 @@ function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray(target) ? target : target.primary; + const to = Array.isArray(target) ? target : target.primary; to.unshift(...actions); } else { - const to = Array.isArray(target) ? target : target.secondary; + const to = Array.isArray(target) ? target : target.secondary; if (to.length > 0) { to.push(new Separator()); @@ -303,6 +303,6 @@ export class SubmenuEntryActionViewItem extends DropdownMenuActionViewItem { } } - super(action, Array.isArray(action.actions) ? action.actions : action.actions(), _contextMenuService, { classNames }); + super(action, action.actions, _contextMenuService, { classNames }); } } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 36781a62572..94d8bff6c76 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -120,7 +120,11 @@ export class MenuId { static readonly CommentTitle = new MenuId('CommentTitle'); static readonly CommentActions = new MenuId('CommentActions'); static readonly NotebookCellTitle = new MenuId('NotebookCellTitle'); + static readonly NotebookCellInsert = new MenuId('NotebookCellInsert'); static readonly NotebookCellBetween = new MenuId('NotebookCellBetween'); + static readonly NotebookCellListTop = new MenuId('NotebookCellTop'); + static readonly NotebookDiffCellMetadataTitle = new MenuId('NotebookDiffCellMetadataTitle'); + static readonly NotebookDiffCellOutputsTitle = new MenuId('NotebookDiffCellOutputsTitle'); static readonly BulkEditTitle = new MenuId('BulkEditTitle'); static readonly BulkEditContext = new MenuId('BulkEditContext'); static readonly TimelineItemContext = new MenuId('TimelineItemContext'); diff --git a/src/vs/platform/contextkey/browser/contextKeyService.ts b/src/vs/platform/contextkey/browser/contextKeyService.ts index 89720995be2..4d9c1afbe55 100644 --- a/src/vs/platform/contextkey/browser/contextKeyService.ts +++ b/src/vs/platform/contextkey/browser/contextKeyService.ts @@ -9,7 +9,6 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContext, IContextKey, IContextKeyChangeEvent, IContextKeyService, IContextKeyServiceTarget, IReadableSet, SET_CONTEXT_COMMAND_ID, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver'; -import { toArray } from 'vs/base/common/arrays'; const KEYBINDING_CONTEXT_ATTR = 'data-keybinding-context'; @@ -102,7 +101,7 @@ class ConfigAwareContextValuesContainer extends Context { this._listener = this._configurationService.onDidChangeConfiguration(event => { if (event.source === ConfigurationTarget.DEFAULT) { // new setting, reset everything - const allKeys = toArray(this._values.keys()); + const allKeys = Array.from(this._values.keys()); this._values.clear(); emitter.fire(new ArrayContextKeyChangeEvent(allKeys)); } else { diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index 55e154645ec..8cdc6954c0a 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -17,6 +17,8 @@ STATIC_VALUES.set('isWindows', isWindows); STATIC_VALUES.set('isWeb', isWeb); STATIC_VALUES.set('isMacNative', isMacintosh && !isWeb); +const hasOwnProperty = Object.prototype.hasOwnProperty; + export const enum ContextKeyExprType { False = 0, True = 1, @@ -27,7 +29,9 @@ export const enum ContextKeyExprType { And = 6, Regex = 7, NotRegex = 8, - Or = 9 + Or = 9, + In = 10, + NotIn = 11, } export interface IContextKeyExprMapper { @@ -36,6 +40,7 @@ export interface IContextKeyExprMapper { mapEquals(key: string, value: any): ContextKeyExpression; mapNotEquals(key: string, value: any): ContextKeyExpression; mapRegex(key: string, regexp: RegExp | null): ContextKeyRegexExpr; + mapIn(key: string, valueKey: string): ContextKeyInExpr; } export interface IContextKeyExpression { @@ -52,7 +57,7 @@ export interface IContextKeyExpression { export type ContextKeyExpression = ( ContextKeyFalseExpr | ContextKeyTrueExpr | ContextKeyDefinedExpr | ContextKeyNotExpr | ContextKeyEqualsExpr | ContextKeyNotEqualsExpr | ContextKeyRegexExpr - | ContextKeyNotRegexExpr | ContextKeyAndExpr | ContextKeyOrExpr + | ContextKeyNotRegexExpr | ContextKeyAndExpr | ContextKeyOrExpr | ContextKeyInExpr | ContextKeyNotInExpr ); export abstract class ContextKeyExpr { @@ -81,6 +86,10 @@ export abstract class ContextKeyExpr { return ContextKeyRegexExpr.create(key, value); } + public static in(key: string, value: string): ContextKeyExpression { + return ContextKeyInExpr.create(key, value); + } + public static not(key: string): ContextKeyExpression { return ContextKeyNotExpr.create(key); } @@ -129,6 +138,11 @@ export abstract class ContextKeyExpr { return ContextKeyRegexExpr.create(pieces[0].trim(), this._deserializeRegexValue(pieces[1], strict)); } + if (serializedOne.indexOf(' in ') >= 0) { + let pieces = serializedOne.split(' in '); + return ContextKeyInExpr.create(pieces[0].trim(), pieces[1].trim()); + } + if (/^\!\s*/.test(serializedOne)) { return ContextKeyNotExpr.create(serializedOne.substr(1).trim()); } @@ -393,6 +407,122 @@ export class ContextKeyEqualsExpr implements IContextKeyExpression { } } +export class ContextKeyInExpr implements IContextKeyExpression { + + public static create(key: string, valueKey: string): ContextKeyInExpr { + return new ContextKeyInExpr(key, valueKey); + } + + public readonly type = ContextKeyExprType.In; + + private constructor(private readonly key: string, private readonly valueKey: string) { + } + + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } + if (this.key < other.key) { + return -1; + } + if (this.key > other.key) { + return 1; + } + if (this.valueKey < other.valueKey) { + return -1; + } + if (this.valueKey > other.valueKey) { + return 1; + } + return 0; + } + + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { + return (this.key === other.key && this.valueKey === other.valueKey); + } + return false; + } + + public evaluate(context: IContext): boolean { + const source = context.getValue(this.valueKey); + + const item = context.getValue(this.key); + + if (Array.isArray(source)) { + return (source.indexOf(item) >= 0); + } + + if (typeof item === 'string' && typeof source === 'object' && source !== null) { + return hasOwnProperty.call(source, item); + } + return false; + } + + public serialize(): string { + return this.key + ' in \'' + this.valueKey + '\''; + } + + public keys(): string[] { + return [this.key, this.valueKey]; + } + + public map(mapFnc: IContextKeyExprMapper): ContextKeyInExpr { + return mapFnc.mapIn(this.key, this.valueKey); + } + + public negate(): ContextKeyExpression { + return ContextKeyNotInExpr.create(this); + } +} + +export class ContextKeyNotInExpr implements IContextKeyExpression { + + public static create(actual: ContextKeyInExpr): ContextKeyNotInExpr { + return new ContextKeyNotInExpr(actual); + } + + public readonly type = ContextKeyExprType.NotIn; + + private constructor(private readonly _actual: ContextKeyInExpr) { + // + } + + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } + return this._actual.cmp(other._actual); + } + + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { + return this._actual.equals(other._actual); + } + return false; + } + + public evaluate(context: IContext): boolean { + return !this._actual.evaluate(context); + } + + public serialize(): string { + throw new Error('Method not implemented.'); + } + + public keys(): string[] { + return this._actual.keys(); + } + + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { + return new ContextKeyNotInExpr(this._actual.map(mapFnc)); + } + + public negate(): ContextKeyExpression { + return this._actual; + } +} + export class ContextKeyNotEqualsExpr implements IContextKeyExpression { public static create(key: string, value: any): ContextKeyExpression { diff --git a/src/vs/platform/contextkey/test/common/contextkey.test.ts b/src/vs/platform/contextkey/test/common/contextkey.test.ts index c7784c888e1..042012f1d33 100644 --- a/src/vs/platform/contextkey/test/common/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/common/contextkey.test.ts @@ -150,4 +150,19 @@ suite('ContextKeyExpr', () => { t('a || b', 'c && d', 'a && c && d || b && c && d'); t('a || b', 'c && d || e', 'a && e || b && e || a && c && d || b && c && d'); }); + + test('ContextKeyInExpr', () => { + const ainb = ContextKeyExpr.deserialize('a in b')!; + assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [3, 2, 1] })), true); + assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2, 3] })), true); + assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2] })), false); + assert.equal(ainb.evaluate(createContext({ 'a': 3 })), false); + assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': null })), false); + assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': ['x'] })), true); + assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': ['y'] })), false); + assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': {} })), false); + assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': false } })), true); + assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': true } })), true); + assert.equal(ainb.evaluate(createContext({ 'a': 'prototype', 'b': {} })), false); + }); }); diff --git a/src/vs/platform/credentials/common/credentials.ts b/src/vs/platform/credentials/common/credentials.ts index 2799abeed19..06af1a01af5 100644 --- a/src/vs/platform/credentials/common/credentials.ts +++ b/src/vs/platform/credentials/common/credentials.ts @@ -5,15 +5,16 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -export const ICredentialsService = createDecorator('ICredentialsService'); - -export interface ICredentialsService { - - readonly _serviceBrand: undefined; - +export interface ICredentialsProvider { getPassword(service: string, account: string): Promise; setPassword(service: string, account: string, password: string): Promise; deletePassword(service: string, account: string): Promise; findPassword(service: string): Promise; findCredentials(service: string): Promise>; } + +export const ICredentialsService = createDecorator('ICredentialsService'); + +export interface ICredentialsService extends ICredentialsProvider { + readonly _serviceBrand: undefined; +} diff --git a/src/vs/platform/credentials/node/credentialsService.ts b/src/vs/platform/credentials/node/credentialsService.ts index cce2f77f6b3..0960eb9a542 100644 --- a/src/vs/platform/credentials/node/credentialsService.ts +++ b/src/vs/platform/credentials/node/credentialsService.ts @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type * as keytar from 'keytar'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { IdleValue } from 'vs/base/common/async'; -type KeytarModule = typeof import('keytar'); export class KeytarCredentialsService implements ICredentialsService { declare readonly _serviceBrand: undefined; - private readonly _keytar = new IdleValue>(() => import('keytar')); + private readonly _keytar = new IdleValue>(() => import('keytar')); async getPassword(service: string, account: string): Promise { const keytar = await this._keytar.value; diff --git a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts new file mode 100644 index 00000000000..923305acd75 --- /dev/null +++ b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug'; +import { IProcessEnvironment } from 'vs/base/common/platform'; +import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; +import { createServer, AddressInfo } from 'net'; +import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; +import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; +import { OpenContext } from 'vs/platform/windows/node/window'; + +export class ElectronExtensionHostDebugBroadcastChannel extends ExtensionHostDebugBroadcastChannel { + + constructor(private windowsMainService: IWindowsMainService) { + super(); + } + + call(ctx: TContext, command: string, arg?: any): Promise { + if (command === 'openExtensionDevelopmentHostWindow') { + return this.openExtensionDevelopmentHostWindow(arg[0], arg[1], arg[2]); + } else { + return super.call(ctx, command, arg); + } + } + + private async openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment, debugRenderer: boolean): Promise { + const pargs = parseArgs(args, OPTIONS); + const extDevPaths = pargs.extensionDevelopmentPath; + if (!extDevPaths) { + return {}; + } + + const [codeWindow] = this.windowsMainService.openExtensionDevelopmentHostWindow(extDevPaths, { + context: OpenContext.API, + cli: pargs, + userEnv: Object.keys(env).length > 0 ? env : undefined + }); + + if (!debugRenderer) { + return {}; + } + + const debug = codeWindow.win.webContents.debugger; + + let listeners = debug.isAttached() ? Infinity : 0; + const server = createServer(listener => { + if (listeners++ === 0) { + debug.attach(); + } + + let closed = false; + const writeMessage = (message: object) => { + if (!closed) { // in case sendCommand promises settle after closed + listener.write(JSON.stringify(message) + '\0'); // null-delimited, CDP-compatible + } + }; + + const onMessage = (_event: Event, method: string, params: unknown, sessionId?: string) => + writeMessage(({ method, params, sessionId })); + + codeWindow.win.on('close', () => { + debug.removeListener('message', onMessage); + listener.end(); + closed = true; + }); + + debug.addListener('message', onMessage); + + let buf = Buffer.alloc(0); + listener.on('data', data => { + buf = Buffer.concat([buf, data]); + for (let delimiter = buf.indexOf(0); delimiter !== -1; delimiter = buf.indexOf(0)) { + let data: { id: number; sessionId: string; params: {} }; + try { + const contents = buf.slice(0, delimiter).toString('utf8'); + buf = buf.slice(delimiter + 1); + data = JSON.parse(contents); + } catch (e) { + console.error('error reading cdp line', e); + } + + // depends on a new API for which electron.d.ts has not been updated: + // @ts-ignore + debug.sendCommand(data.method, data.params, data.sessionId) + .then((result: object) => writeMessage({ id: data.id, sessionId: data.sessionId, result })) + .catch((error: Error) => writeMessage({ id: data.id, sessionId: data.sessionId, error: { code: 0, message: error.message } })); + } + }); + + listener.on('error', err => { + console.error('error on cdp pipe:', err); + }); + + listener.on('close', () => { + closed = true; + if (--listeners === 0) { + debug.detach(); + } + }); + }); + + await new Promise(r => server.listen(0, r)); + codeWindow.win.on('close', () => server.close()); + + return { rendererDebugPort: (server.address() as AddressInfo).port }; + } +} diff --git a/src/vs/platform/electron/common/electron.ts b/src/vs/platform/electron/common/electron.ts index 1d65ceea6c6..759bcd96ab3 100644 --- a/src/vs/platform/electron/common/electron.ts +++ b/src/vs/platform/electron/common/electron.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; -import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, SaveDialogOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogReturnValue, CrashReporterStartOptions, MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes'; +import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, SaveDialogOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogReturnValue, MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes'; import { IOpenedWindow, IWindowOpenable, IOpenEmptyWindowOptions, IOpenWindowOptions } from 'vs/platform/windows/common/windows'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; @@ -98,7 +98,6 @@ export interface ICommonElectronService { // Development openDevTools(options?: OpenDevToolsOptions): Promise; toggleDevTools(): Promise; - startCrashReporter(options: CrashReporterStartOptions): Promise; sendInputEvent(event: MouseInputEvent): Promise; // Connectivity diff --git a/src/vs/platform/electron/electron-main/electronMainService.ts b/src/vs/platform/electron/electron-main/electronMainService.ts index 0976846791f..0c2f9a735ca 100644 --- a/src/vs/platform/electron/electron-main/electronMainService.ts +++ b/src/vs/platform/electron/electron-main/electronMainService.ts @@ -5,7 +5,7 @@ import { Event } from 'vs/base/common/event'; import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; -import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, CrashReporterStartOptions, crashReporter, Menu, BrowserWindow, app, clipboard, powerMonitor } from 'electron'; +import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, Menu, BrowserWindow, app, clipboard, powerMonitor } from 'electron'; import { OpenContext } from 'vs/platform/windows/node/window'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows'; @@ -20,7 +20,6 @@ import { dirExists } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ILogService } from 'vs/platform/log/common/log'; import { INativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; import { MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes'; import { totalmem } from 'os'; @@ -38,8 +37,7 @@ export class ElectronMainService implements IElectronMainService { @IDialogMainService private readonly dialogMainService: IDialogMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @IEnvironmentService private readonly environmentService: INativeEnvironmentService, - @ITelemetryService private readonly telemetryService: ITelemetryService, - @ILogService private readonly logService: ILogService + @ITelemetryService private readonly telemetryService: ITelemetryService ) { } @@ -479,12 +477,6 @@ export class ElectronMainService implements IElectronMainService { } } - async startCrashReporter(windowId: number | undefined, options: CrashReporterStartOptions): Promise { - this.logService.trace('ElectronMainService#crashReporter', JSON.stringify(options)); - - crashReporter.start(options); - } - async sendInputEvent(windowId: number | undefined, event: MouseInputEvent): Promise { const window = this.windowById(windowId); if (window && (event.type === 'mouseDown' || event.type === 'mouseUp')) { diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 6c660f87ed8..6ff81a44fd2 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -23,7 +23,7 @@ export interface IEnvironmentService { // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // NOTE: DO NOT ADD ANY OTHER PROPERTY INTO THE COLLECTION HERE - // UNLESS THIS PROPERTY IS SUPPORTED BOTH IN WEB AND DESKTOP!!!! + // UNLESS THIS PROPERTY IS SUPPORTED BOTH IN WEB AND NATIVE!!!! // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! readonly _serviceBrand: undefined; @@ -70,6 +70,6 @@ export interface IEnvironmentService { // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // NOTE: DO NOT ADD ANY OTHER PROPERTY INTO THE COLLECTION HERE - // UNLESS THIS PROPERTY IS SUPPORTED BOTH IN WEB AND DESKTOP!!!! + // UNLESS THIS PROPERTY IS SUPPORTED BOTH IN WEB AND NATIVE!!!! // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 2379b626c81..92dd2bcf87d 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -64,6 +64,7 @@ export interface ParsedArgs { 'disable-updates'?: boolean; 'disable-crash-reporter'?: boolean; 'crash-reporter-directory'?: string; + 'crash-reporter-id'?: string; 'skip-add-to-recently-opened'?: boolean; 'max-memory'?: string; 'file-write'?: boolean; @@ -182,6 +183,7 @@ export const OPTIONS: OptionDescriptions> = { 'disable-updates': { type: 'boolean' }, 'disable-crash-reporter': { type: 'boolean' }, 'crash-reporter-directory': { type: 'string' }, + 'crash-reporter-id': { type: 'string' }, 'disable-user-env-probe': { type: 'boolean' }, 'skip-add-to-recently-opened': { type: 'boolean' }, 'unity-launch': { type: 'boolean' }, diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 5c0dc4ad4ae..45d5ec2cc02 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -256,7 +256,7 @@ export class EnvironmentService implements INativeEnvironmentService { get serviceMachineIdResource(): URI { return resources.joinPath(URI.file(this.userDataPath), 'machineid'); } get disableUpdates(): boolean { return !!this._args['disable-updates']; } - get disableCrashReporter(): boolean { return !!this._args['disable-crash-reporter']; } + get crashReporterId(): string | undefined { return this._args['crash-reporter-id']; } get crashReporterDirectory(): string | undefined { return this._args['crash-reporter-directory']; } get driverHandle(): string | undefined { return this._args['driver']; } diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 52949788147..513ce8e92a7 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -115,7 +115,8 @@ const PropertyType = { Dependency: 'Microsoft.VisualStudio.Code.ExtensionDependencies', ExtensionPack: 'Microsoft.VisualStudio.Code.ExtensionPack', Engine: 'Microsoft.VisualStudio.Code.Engine', - LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages' + LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages', + WebExtension: 'Microsoft.VisualStudio.Code.WebExtension' }; interface ICriterium { @@ -266,6 +267,11 @@ function getIsPreview(flags: string): boolean { return flags.indexOf('preview') !== -1; } +function getIsWebExtension(version: IRawGalleryExtensionVersion): boolean { + const webExtensionProperty = version.properties ? version.properties.find(p => p.key === PropertyType.WebExtension) : undefined; + return !!webExtensionProperty && webExtensionProperty.value === 'true'; +} + function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, index: number, query: Query, querySource?: string): IGalleryExtension { const assets = { manifest: getVersionAsset(version, AssetType.Manifest), @@ -301,7 +307,8 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller dependencies: getExtensions(version, PropertyType.Dependency), extensionPack: getExtensions(version, PropertyType.ExtensionPack), engine: getEngine(version), - localizedLanguages: getLocalizedLanguages(version) + localizedLanguages: getLocalizedLanguages(version), + webExtension: getIsWebExtension(version) }, /* __GDPR__FRAGMENT__ "GalleryExtensionTelemetryData2" : { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 34811829b09..609e3a501e2 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -19,6 +19,7 @@ export interface IGalleryExtensionProperties { extensionPack?: string[]; engine?: string; localizedLanguages?: string[]; + webExtension?: boolean; } export interface IGalleryExtensionAsset { @@ -204,6 +205,7 @@ export interface IExtensionManagementService { unzip(zipLocation: URI): Promise; getManifest(vsix: URI): Promise; install(vsix: URI, isMachineScoped?: boolean): Promise; + canInstall(extension: IGalleryExtension): Promise; installFromGallery(extension: IGalleryExtension, isMachineScoped?: boolean): Promise; uninstall(extension: ILocalExtension, force?: boolean): Promise; reinstallFromGallery(extension: ILocalExtension): Promise; @@ -239,6 +241,7 @@ export type IExecutableBasedExtensionTip = { readonly extensionId: string, readonly extensionName: string, readonly isExtensionPack: boolean, + readonly exeName: string, readonly exeFriendlyName: string, readonly windowsPath?: string, }; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index d497780449a..048aa90adf3 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -63,6 +63,7 @@ export class ExtensionManagementChannel implements IServerChannel { case 'unzip': return this.service.unzip(transformIncomingURI(args[0], uriTransformer)); case 'install': return this.service.install(transformIncomingURI(args[0], uriTransformer)); case 'getManifest': return this.service.getManifest(transformIncomingURI(args[0], uriTransformer)); + case 'canInstall': return this.service.canInstall(args[0]); case 'installFromGallery': return this.service.installFromGallery(args[0]); case 'uninstall': return this.service.uninstall(transformIncomingExtension(args[0], uriTransformer), args[1]); case 'reinstallFromGallery': return this.service.reinstallFromGallery(transformIncomingExtension(args[0], uriTransformer)); @@ -104,6 +105,10 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer return Promise.resolve(this.channel.call('getManifest', [vsix])); } + async canInstall(extension: IGalleryExtension): Promise { + return true; + } + installFromGallery(extension: IGalleryExtension): Promise { return Promise.resolve(this.channel.call('installFromGallery', [extension])).then(local => transformIncomingExtension(local, null)); } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index bfd0fbd7877..badeef4aa5d 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -193,7 +193,7 @@ export class ExtensionManagementService extends Disposable implements IExtension this._onInstallExtension.fire({ identifier, zipPath }); return this.getGalleryMetadata(getGalleryExtensionId(manifest.publisher, manifest.name)) .then( - metadata => this.installFromZipPath(identifierWithVersion, zipPath, { ...metadata, isMachineScoped }, operation, token), + metadata => this.installFromZipPath(identifierWithVersion, zipPath, isMachineScoped ? { ...metadata, isMachineScoped } : metadata, operation, token), () => this.installFromZipPath(identifierWithVersion, zipPath, isMachineScoped ? { isMachineScoped } : undefined, operation, token)) .then( local => { this.logService.info('Successfully installed the extension:', identifier.id); return local; }, @@ -238,6 +238,10 @@ export class ExtensionManagementService extends Disposable implements IExtension )); } + async canInstall(extension: IGalleryExtension): Promise { + return true; + } + async installFromGallery(extension: IGalleryExtension, isMachineScoped?: boolean): Promise { if (!this.galleryService.isEnabled()) { return Promise.reject(new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"))); diff --git a/src/vs/platform/extensionManagement/node/extensionTipsService.ts b/src/vs/platform/extensionManagement/node/extensionTipsService.ts index 5780216da37..3dd401c785b 100644 --- a/src/vs/platform/extensionManagement/node/extensionTipsService.ts +++ b/src/vs/platform/extensionManagement/node/extensionTipsService.ts @@ -105,6 +105,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService { extensionId, extensionName, isExtensionPack, + exeName, exeFriendlyName: extensionTip.exeFriendlyName, windowsPath: extensionTip.windowsPath, }); diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 6024a71fb10..b11f8baf57a 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -108,6 +108,11 @@ export interface ICodeActionContribution { readonly actions: readonly ICodeActionContributionAction[]; } +export interface IAuthenticationContribution { + readonly id: string; + readonly label: string; +} + export interface IExtensionContributions { commands?: ICommand[]; configuration?: IConfiguration | IConfiguration[]; @@ -126,6 +131,7 @@ export interface IExtensionContributions { localizations?: ILocalization[]; readonly customEditors?: readonly IWebviewEditor[]; readonly codeActions?: readonly ICodeActionContribution[]; + authentication?: IAuthenticationContribution[]; } export type ExtensionKind = 'ui' | 'workspace' | 'web'; @@ -268,6 +274,10 @@ export function isLanguagePackExtension(manifest: IExtensionManifest): boolean { return manifest.contributes && manifest.contributes.localizations ? manifest.contributes.localizations.length > 0 : false; } +export function isAuthenticaionProviderExtension(manifest: IExtensionManifest): boolean { + return manifest.contributes && manifest.contributes.authentication ? manifest.contributes.authentication.length > 0 : false; +} + export interface IScannedExtension { readonly identifier: IExtensionIdentifier; readonly location: URI; diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 39b57391424..2b046ac4a59 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -867,7 +867,7 @@ export function whenProviderRegistered(file: URI, fileService: IFileService): Pr } /** - * Desktop only: limits for memory sizes + * Native only: limits for memory sizes */ export const MIN_MAX_MEMORY_SIZE_MB = 2048; export const FALLBACK_MAX_MEMORY_SIZE_MB = 4096; diff --git a/src/vs/platform/keybinding/common/abstractKeybindingService.ts b/src/vs/platform/keybinding/common/abstractKeybindingService.ts index 1fce530772a..c6e754997f1 100644 --- a/src/vs/platform/keybinding/common/abstractKeybindingService.ts +++ b/src/vs/platform/keybinding/common/abstractKeybindingService.ts @@ -17,6 +17,7 @@ import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKe import { INotificationService } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; +import { ILogService } from 'vs/platform/log/common/log'; interface CurrentChord { keypress: string; @@ -44,6 +45,7 @@ export abstract class AbstractKeybindingService extends Disposable implements IK protected _commandService: ICommandService, protected _telemetryService: ITelemetryService, private _notificationService: INotificationService, + protected _logService: ILogService, ) { super(); @@ -177,6 +179,8 @@ export abstract class AbstractKeybindingService extends Disposable implements IK const keypressLabel = keybinding.getLabel(); const resolveResult = this._getResolver().resolve(contextValue, currentChord, firstPart); + this._logService.trace('KeybindingService#dispatch', keypressLabel, resolveResult?.commandId); + if (resolveResult && resolveResult.enterChord) { shouldPreventDefault = true; this._enterChordMode(firstPart, keypressLabel); diff --git a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts index 286ea8a57ae..5149a9202cc 100644 --- a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts +++ b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts @@ -16,6 +16,7 @@ import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayo import { INotification, INotificationService, IPromptChoice, IPromptOptions, NoOpNotification, IStatusMessageOptions } from 'vs/platform/notification/common/notification'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { Disposable } from 'vs/base/common/lifecycle'; +import { NullLogService } from 'vs/platform/log/common/log'; function createContext(ctx: any) { return { @@ -36,7 +37,7 @@ suite('AbstractKeybindingService', () => { commandService: ICommandService, notificationService: INotificationService ) { - super(contextKeyService, commandService, NullTelemetryService, notificationService); + super(contextKeyService, commandService, NullTelemetryService, notificationService, new NullLogService()); this._resolver = resolver; } diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 233f1690c98..f1b54f87e9d 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -70,14 +70,18 @@ export class LaunchMainService implements ILaunchMainService { @IConfigurationService private readonly configurationService: IConfigurationService ) { } - start(args: ParsedArgs, userEnv: IProcessEnvironment): Promise { + async start(args: ParsedArgs, userEnv: IProcessEnvironment): Promise { this.logService.trace('Received data from other instance: ', args, userEnv); - const urlsToOpen = parseOpenUrl(args); + // Since we now start to open a window, make sure the app has focus. + // Focussing a window will not ensure that the application itself + // has focus, so we use the `steal: true` hint to force focus. + app.focus({ steal: true }); // Check early for open-url which is handled in URL service + const urlsToOpen = parseOpenUrl(args); if (urlsToOpen.length) { - let whenWindowReady: Promise = Promise.resolve(null); + let whenWindowReady: Promise = Promise.resolve(); // Create a window if there is none if (this.windowsMainService.getWindowCount() === 0) { @@ -91,12 +95,12 @@ export class LaunchMainService implements ILaunchMainService { this.urlService.open(url); } }); - - return Promise.resolve(undefined); } // Otherwise handle in windows service - return this.startOpenWindow(args, userEnv); + else { + return this.startOpenWindow(args, userEnv); + } } private startOpenWindow(args: ParsedArgs, userEnv: IProcessEnvironment): Promise { diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index e4cd33ab6e8..bf78261135d 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -7,15 +7,15 @@ import * as nls from 'vs/nls'; import { isMacintosh, language } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { app, shell, Menu, MenuItem, BrowserWindow, MenuItemConstructorOptions, WebContents, Event, KeyboardEvent } from 'electron'; -import { getTitleBarStyle, IWindowOpenable } from 'vs/platform/windows/common/windows'; -import { OpenContext, IRunActionInWindowRequest, IRunKeybindingInWindowRequest } from 'vs/platform/windows/node/window'; +import { getTitleBarStyle, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, IWindowOpenable } from 'vs/platform/windows/common/windows'; +import { OpenContext } from 'vs/platform/windows/node/window'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUpdateService, StateType } from 'vs/platform/update/common/update'; import product from 'vs/platform/product/common/product'; import { RunOnceScheduler } from 'vs/base/common/async'; import { ILogService } from 'vs/platform/log/common/log'; -import { mnemonicMenuLabel as baseMnemonicLabel } from 'vs/base/common/labels'; +import { mnemonicMenuLabel } from 'vs/base/common/labels'; import { IWindowsMainService, IWindowsCountChangedEvent } from 'vs/platform/windows/electron-main/windows'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction, IMenubarMenu, isMenubarMenuItemUriAction } from 'vs/platform/menubar/common/menubar'; @@ -754,9 +754,11 @@ export class Menubar { } if (invocation.type === 'commandId') { - activeWindow.sendWhenReady('vscode:runAction', { id: invocation.commandId, from: 'menu' } as IRunActionInWindowRequest); + const runActionPayload: INativeRunActionInWindowRequest = { id: invocation.commandId, from: 'menu' }; + activeWindow.sendWhenReady('vscode:runAction', runActionPayload); } else { - activeWindow.sendWhenReady('vscode:runKeybinding', { userSettingsLabel: invocation.userSettingsLabel } as IRunKeybindingInWindowRequest); + const runKeybindingPayload: INativeRunKeybindingInWindowRequest = { userSettingsLabel: invocation.userSettingsLabel }; + activeWindow.sendWhenReady('vscode:runKeybinding', runKeybindingPayload); } } else { this.logService.trace('menubar#runActionInRenderer: no active window found', invocation); @@ -821,7 +823,7 @@ export class Menubar { } private mnemonicLabel(label: string): string { - return baseMnemonicLabel(label, !this.currentEnableMenuBarMnemonics); + return mnemonicMenuLabel(label, !this.currentEnableMenuBarMnemonics); } } diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 3370a608b4b..bb33203d172 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -20,7 +20,7 @@ if (isWeb) { // Running out of sources if (Object.keys(product).length === 0) { Object.assign(product, { - version: '1.48.0-dev', + version: '1.49.0-dev', nameLong: 'Visual Studio Code Web Dev', nameShort: 'VSCode Web Dev', urlProtocol: 'code-oss', diff --git a/src/vs/platform/product/common/productService.ts b/src/vs/platform/product/common/productService.ts index 040c869d94c..d1cb00a6d63 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts @@ -71,7 +71,7 @@ export interface IProductConfiguration { }; readonly extensionTips?: { [id: string]: string; }; - readonly extensionImportantTips?: { [id: string]: { name: string; pattern: string; isExtensionPack?: boolean }; }; + readonly extensionImportantTips?: IStringDictionary; readonly configBasedExtensionTips?: { [id: string]: IConfigBasedExtensionTip; }; readonly exeBasedExtensionTips?: { [id: string]: IExeBasedExtensionTip; }; readonly remoteExtensionTips?: { [remoteName: string]: IRemoteExtensionTip; }; @@ -127,6 +127,8 @@ export interface IProductConfiguration { readonly 'configurationSync.store'?: ConfigurationSyncStore; } +export type ImportantExtensionTip = { name: string; languages?: string[]; pattern?: string; isExtensionPack?: boolean }; + export interface IAppCenterConfiguration { readonly 'win32-ia32': string; readonly 'win32-x64': string; diff --git a/src/vs/platform/remote/common/remoteAgentConnection.ts b/src/vs/platform/remote/common/remoteAgentConnection.ts index 2185bb5228c..18d3d04fd20 100644 --- a/src/vs/platform/remote/common/remoteAgentConnection.ts +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts @@ -16,6 +16,9 @@ import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async import { ILogService } from 'vs/platform/log/common/log'; import { IIPCLogger } from 'vs/base/parts/ipc/common/ipc'; +const INITIAL_CONNECT_TIMEOUT = 120 * 1000 /* 120s */; +const RECONNECT_TIMEOUT = 30 * 1000 /* 30s */; + export const enum ConnectionType { Management = 1, ExtensionHost = 2, @@ -277,7 +280,7 @@ export async function connectRemoteAgentManagement(options: IConnectionOptions, try { const reconnectionToken = generateUuid(); const simpleOptions = await resolveConnectionOptions(options, reconnectionToken, null); - const { protocol } = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentManagement(simpleOptions), 30 * 1000 /*30s*/); + const { protocol } = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentManagement(simpleOptions), INITIAL_CONNECT_TIMEOUT); return new ManagementPersistentConnection(options, remoteAuthority, clientId, reconnectionToken, protocol); } catch (err) { options.logService.error(`[remote-connection] An error occurred in the very first connect attempt, it will be treated as a permanent error! Error:`); @@ -291,7 +294,7 @@ export async function connectRemoteAgentExtensionHost(options: IConnectionOption try { const reconnectionToken = generateUuid(); const simpleOptions = await resolveConnectionOptions(options, reconnectionToken, null); - const { protocol, debugPort } = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentExtensionHost(simpleOptions, startArguments), 30 * 1000 /*30s*/); + const { protocol, debugPort } = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentExtensionHost(simpleOptions, startArguments), INITIAL_CONNECT_TIMEOUT); return new ExtensionHostPersistentConnection(options, startArguments, reconnectionToken, protocol, debugPort); } catch (err) { options.logService.error(`[remote-connection] An error occurred in the very first connect attempt, it will be treated as a permanent error! Error:`); @@ -303,7 +306,7 @@ export async function connectRemoteAgentExtensionHost(options: IConnectionOption export async function connectRemoteAgentTunnel(options: IConnectionOptions, tunnelRemotePort: number): Promise { const simpleOptions = await resolveConnectionOptions(options, generateUuid(), null); - const protocol = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentTunnel(simpleOptions, { port: tunnelRemotePort }), 30 * 1000 /*30s*/); + const protocol = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentTunnel(simpleOptions, { port: tunnelRemotePort }), INITIAL_CONNECT_TIMEOUT); return protocol; } @@ -434,7 +437,7 @@ abstract class PersistentConnection extends Disposable { this._options.logService.info(`${logPrefix} resolving connection...`); const simpleOptions = await resolveConnectionOptions(this._options, this.reconnectionToken, this.protocol); this._options.logService.info(`${logPrefix} connecting to ${simpleOptions.host}:${simpleOptions.port}...`); - await connectWithTimeLimit(simpleOptions.logService, this._reconnect(simpleOptions), 30 * 1000 /*30s*/); + await connectWithTimeLimit(simpleOptions.logService, this._reconnect(simpleOptions), RECONNECT_TIMEOUT); this._options.logService.info(`${logPrefix} reconnected!`); this._onDidStateChange.fire(new ConnectionGainEvent()); @@ -453,24 +456,24 @@ abstract class PersistentConnection extends Disposable { break; } if (RemoteAuthorityResolverError.isTemporarilyNotAvailable(err)) { - this._options.logService.info(`${logPrefix} A temporarily not available error occured while trying to reconnect, will try again...`); + this._options.logService.info(`${logPrefix} A temporarily not available error occurred while trying to reconnect, will try again...`); this._options.logService.trace(err); // try again! continue; } if ((err.code === 'ETIMEDOUT' || err.code === 'ENETUNREACH' || err.code === 'ECONNREFUSED' || err.code === 'ECONNRESET') && err.syscall === 'connect') { - this._options.logService.info(`${logPrefix} A network error occured while trying to reconnect, will try again...`); + this._options.logService.info(`${logPrefix} A network error occurred while trying to reconnect, will try again...`); this._options.logService.trace(err); // try again! continue; } if (isPromiseCanceledError(err)) { - this._options.logService.info(`${logPrefix} A promise cancelation error occured while trying to reconnect, will try again...`); + this._options.logService.info(`${logPrefix} A promise cancelation error occurred while trying to reconnect, will try again...`); this._options.logService.trace(err); // try again! continue; } - this._options.logService.error(`${logPrefix} An unknown error occured while trying to reconnect, since this is an unknown case, it will be treated as a permanent error! Will give up now! Error:`); + this._options.logService.error(`${logPrefix} An unknown error occurred while trying to reconnect, since this is an unknown case, it will be treated as a permanent error! Will give up now! Error:`); this._options.logService.error(err); PersistentConnection.triggerPermanentFailure(); break; diff --git a/src/vs/platform/remote/common/tunnel.ts b/src/vs/platform/remote/common/tunnel.ts index c2275c2948c..52dfacbc403 100644 --- a/src/vs/platform/remote/common/tunnel.ts +++ b/src/vs/platform/remote/common/tunnel.ts @@ -56,7 +56,13 @@ export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: }; } +export function isLocalhost(host: string): boolean { + return host === 'localhost' || host === '127.0.0.1'; +} +function getOtherLocalhost(host: string): string | undefined { + return (host === 'localhost') ? '127.0.0.1' : ((host === '127.0.0.1') ? 'localhost' : undefined); +} export abstract class AbstractTunnelService implements ITunnelService { declare readonly _serviceBrand: undefined; @@ -107,7 +113,7 @@ export abstract class AbstractTunnelService implements ITunnelService { return undefined; } - if (!remoteHost || (remoteHost === '127.0.0.1')) { + if (!remoteHost) { remoteHost = 'localhost'; } @@ -174,13 +180,29 @@ export abstract class AbstractTunnelService implements ITunnelService { this._tunnels.get(remoteHost)!.set(remotePort, { refcount: 1, value: tunnel }); } + protected getTunnelFromMap(remoteHost: string, remotePort: number): { refcount: number, readonly value: Promise } | undefined { + const otherLocalhost = getOtherLocalhost(remoteHost); + let portMap: Map }> | undefined; + if (otherLocalhost) { + const firstMap = this._tunnels.get(remoteHost); + const secondMap = this._tunnels.get(otherLocalhost); + if (firstMap && secondMap) { + portMap = new Map([...Array.from(firstMap.entries()), ...Array.from(secondMap.entries())]); + } else { + portMap = firstMap ?? secondMap; + } + } else { + portMap = this._tunnels.get(remoteHost); + } + return portMap ? portMap.get(remotePort) : undefined; + } + protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined; } export class TunnelService extends AbstractTunnelService { protected retainOrCreateTunnel(_addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number | undefined): Promise | undefined { - const portMap = this._tunnels.get(remoteHost); - const existing = portMap ? portMap.get(remotePort) : undefined; + const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { ++existing.refcount; return existing.value; diff --git a/src/vs/platform/remote/node/tunnelService.ts b/src/vs/platform/remote/node/tunnelService.ts index 983401be417..c8d84bdeff4 100644 --- a/src/vs/platform/remote/node/tunnelService.ts +++ b/src/vs/platform/remote/node/tunnelService.ts @@ -86,7 +86,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { this.tunnelLocalPort = address.port; await this._barrier.wait(); - this.localAddress = 'localhost:' + address.port; + this.localAddress = `${this.tunnelRemoteHost === '127.0.0.1' ? '127.0.0.1' : 'localhost'}:${address.port}`; return this; } @@ -132,8 +132,7 @@ export class TunnelService extends AbstractTunnelService { } protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined { - const portMap = this._tunnels.get(remoteHost); - const existing = portMap ? portMap.get(remotePort) : undefined; + const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { ++existing.refcount; return existing.value; diff --git a/src/vs/platform/severityIcon/common/severityIcon.ts b/src/vs/platform/severityIcon/common/severityIcon.ts index f09bc4b255b..8b4697e84b9 100644 --- a/src/vs/platform/severityIcon/common/severityIcon.ts +++ b/src/vs/platform/severityIcon/common/severityIcon.ts @@ -20,8 +20,9 @@ export namespace SeverityIcon { return Codicon.warning.classNames; case Severity.Error: return Codicon.error.classNames; + default: + return ''; } - return ''; } } diff --git a/src/vs/platform/storage/browser/storageService.ts b/src/vs/platform/storage/browser/storageService.ts index 59b1baf9120..ab3fd347b69 100644 --- a/src/vs/platform/storage/browser/storageService.ts +++ b/src/vs/platform/storage/browser/storageService.ts @@ -5,7 +5,7 @@ import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; -import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage } from 'vs/platform/storage/common/storage'; +import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage, IS_NEW_KEY } from 'vs/platform/storage/common/storage'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; import { IFileService, FileChangeType } from 'vs/platform/files/common/files'; @@ -20,8 +20,6 @@ export class BrowserStorageService extends Disposable implements IStorageService declare readonly _serviceBrand: undefined; - private static readonly WORKSPACE_IS_NEW_KEY = '__$__isNewStorageMarker'; - private readonly _onDidChangeStorage = this._register(new Emitter()); readonly onDidChangeStorage = this._onDidChangeStorage.event; @@ -82,12 +80,20 @@ export class BrowserStorageService extends Disposable implements IStorageService this.globalStorage.init() ]); - // Check to see if this is the first time we are "opening" this workspace - const firstOpen = this.workspaceStorage.getBoolean(BrowserStorageService.WORKSPACE_IS_NEW_KEY); + // Check to see if this is the first time we are "opening" the application + const firstOpen = this.globalStorage.getBoolean(IS_NEW_KEY); if (firstOpen === undefined) { - this.workspaceStorage.set(BrowserStorageService.WORKSPACE_IS_NEW_KEY, true); + this.globalStorage.set(IS_NEW_KEY, true); } else if (firstOpen) { - this.workspaceStorage.set(BrowserStorageService.WORKSPACE_IS_NEW_KEY, false); + this.globalStorage.set(IS_NEW_KEY, false); + } + + // Check to see if this is the first time we are "opening" this workspace + const firstWorkspaceOpen = this.workspaceStorage.getBoolean(IS_NEW_KEY); + if (firstWorkspaceOpen === undefined) { + this.workspaceStorage.set(IS_NEW_KEY, true); + } else if (firstWorkspaceOpen) { + this.workspaceStorage.set(IS_NEW_KEY, false); } // In the browser we do not have support for long running unload sequences. As such, @@ -189,8 +195,8 @@ export class BrowserStorageService extends Disposable implements IStorageService this.dispose(); } - isNew(scope: StorageScope.WORKSPACE): boolean { - return this.getBoolean(BrowserStorageService.WORKSPACE_IS_NEW_KEY, scope) === true; + isNew(scope: StorageScope): boolean { + return this.getBoolean(IS_NEW_KEY, scope) === true; } dispose(): void { diff --git a/src/vs/platform/storage/common/storage.ts b/src/vs/platform/storage/common/storage.ts index 1623957cb18..6611f1dae42 100644 --- a/src/vs/platform/storage/common/storage.ts +++ b/src/vs/platform/storage/common/storage.ts @@ -9,6 +9,8 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; +export const IS_NEW_KEY = '__$__isNewStorageMarker'; + export const IStorageService = createDecorator('storageService'); export enum WillSaveStateReason { @@ -104,12 +106,11 @@ export interface IStorageService { migrate(toWorkspace: IWorkspaceInitializationPayload): Promise; /** - * Wether the storage for the given scope was created during this session or + * Whether the storage for the given scope was created during this session or * existed before. * - * Note: currently only implemented for `WORKSPACE` scope. */ - isNew(scope: StorageScope.WORKSPACE): boolean; + isNew(scope: StorageScope): boolean; /** * Allows to flush state, e.g. in cases where a shutdown is @@ -239,6 +240,8 @@ export class InMemoryStorageService extends Disposable implements IStorageServic isNew(): boolean { return true; // always new when in-memory } + + async close(): Promise { } } export async function logStorage(global: Map, workspace: Map, globalPath: string, workspacePath: string): Promise { diff --git a/src/vs/platform/storage/node/storageIpc.ts b/src/vs/platform/storage/node/storageIpc.ts index 9db4e9afcda..8645255d9e7 100644 --- a/src/vs/platform/storage/node/storageIpc.ts +++ b/src/vs/platform/storage/node/storageIpc.ts @@ -10,7 +10,7 @@ import { IUpdateRequest, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/b import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { generateUuid } from 'vs/base/common/uuid'; -import { instanceStorageKey, firstSessionDateStorageKey, lastSessionDateStorageKey, currentSessionDateStorageKey, crashReporterIdStorageKey } from 'vs/platform/telemetry/common/telemetry'; +import { instanceStorageKey, firstSessionDateStorageKey, lastSessionDateStorageKey, currentSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; type Key = string; type Value = string; @@ -49,16 +49,6 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC this.logService.error(`[storage] init(): Unable to init global storage due to ${error}`); } - // This is unique to the application instance and thereby - // should be written from the main process once. - // - // THIS SHOULD NEVER BE SENT TO TELEMETRY. - // - const crashReporterId = this.storageMainService.get(crashReporterIdStorageKey, undefined); - if (crashReporterId === undefined) { - this.storageMainService.store(crashReporterIdStorageKey, generateUuid()); - } - // Apply global telemetry values as part of the initialization // These are global across all windows and thereby should be // written from the main process once. diff --git a/src/vs/platform/storage/node/storageMainService.ts b/src/vs/platform/storage/node/storageMainService.ts index 1abfd328ce6..9ba93bb462a 100644 --- a/src/vs/platform/storage/node/storageMainService.ts +++ b/src/vs/platform/storage/node/storageMainService.ts @@ -12,6 +12,7 @@ import { INativeEnvironmentService } from 'vs/platform/environment/node/environm import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage'; import { Storage, IStorage, InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage'; import { join } from 'vs/base/common/path'; +import { IS_NEW_KEY } from 'vs/platform/storage/common/storage'; export const IStorageMainService = createDecorator('storageMainService'); @@ -135,7 +136,7 @@ export class StorageMainService extends Disposable implements IStorageMainServic return this.initializePromise; } - private doInitialize(): Promise { + private async doInitialize(): Promise { this.storage.dispose(); this.storage = new Storage(new SQLiteStorageDatabase(this.storagePath, { logging: this.createLogginOptions() @@ -143,7 +144,15 @@ export class StorageMainService extends Disposable implements IStorageMainServic this._register(this.storage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key }))); - return this.storage.init(); + await this.storage.init(); + + // Check to see if this is the first time we are "opening" the application + const firstOpen = this.storage.getBoolean(IS_NEW_KEY); + if (firstOpen === undefined) { + this.storage.set(IS_NEW_KEY, true); + } else if (firstOpen) { + this.storage.set(IS_NEW_KEY, false); + } } get(key: string, fallbackValue: string): string; diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts index 75514fe5a4f..ac657056aa6 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts @@ -6,7 +6,7 @@ import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; -import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage } from 'vs/platform/storage/common/storage'; +import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage, IS_NEW_KEY } from 'vs/platform/storage/common/storage'; import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage'; import { Storage, IStorageDatabase, IStorage, StorageHint } from 'vs/base/parts/storage/common/storage'; import { mark } from 'vs/base/common/performance'; @@ -25,8 +25,6 @@ export class NativeStorageService extends Disposable implements IStorageService private static readonly WORKSPACE_STORAGE_NAME = 'state.vscdb'; private static readonly WORKSPACE_META_NAME = 'workspace.json'; - private static readonly WORKSPACE_IS_NEW_KEY = '__$__isNewStorageMarker'; - private readonly _onDidChangeStorage = this._register(new Emitter()); readonly onDidChangeStorage = this._onDidChangeStorage.event; @@ -108,11 +106,11 @@ export class NativeStorageService extends Disposable implements IStorageService await workspaceStorage.init(); // Check to see if this is the first time we are "opening" this workspace - const firstOpen = workspaceStorage.getBoolean(NativeStorageService.WORKSPACE_IS_NEW_KEY); - if (firstOpen === undefined) { - workspaceStorage.set(NativeStorageService.WORKSPACE_IS_NEW_KEY, result.wasCreated); - } else if (firstOpen) { - workspaceStorage.set(NativeStorageService.WORKSPACE_IS_NEW_KEY, false); + const firstWorkspaceOpen = workspaceStorage.getBoolean(IS_NEW_KEY); + if (firstWorkspaceOpen === undefined) { + workspaceStorage.set(IS_NEW_KEY, result.wasCreated); + } else if (firstWorkspaceOpen) { + workspaceStorage.set(IS_NEW_KEY, false); } } finally { mark('didInitWorkspaceStorage'); @@ -281,7 +279,7 @@ export class NativeStorageService extends Disposable implements IStorageService return this.createWorkspaceStorage(newWorkspaceStoragePath).init(); } - isNew(scope: StorageScope.WORKSPACE): boolean { - return this.getBoolean(NativeStorageService.WORKSPACE_IS_NEW_KEY, scope) === true; + isNew(scope: StorageScope): boolean { + return this.getBoolean(IS_NEW_KEY, scope) === true; } } diff --git a/src/vs/platform/telemetry/common/telemetry.ts b/src/vs/platform/telemetry/common/telemetry.ts index 523f81e8d0b..1acf9259110 100644 --- a/src/vs/platform/telemetry/common/telemetry.ts +++ b/src/vs/platform/telemetry/common/telemetry.ts @@ -57,4 +57,3 @@ export const currentSessionDateStorageKey = 'telemetry.currentSessionDate'; export const firstSessionDateStorageKey = 'telemetry.firstSessionDate'; export const lastSessionDateStorageKey = 'telemetry.lastSessionDate'; export const machineIdKey = 'telemetry.machineId'; -export const crashReporterIdStorageKey = 'crashReporter.guid'; diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index 1e1c6fcb583..3a8d6feef57 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -89,7 +89,7 @@ export class TelemetryService implements ITelemetryService { } private _updateUserOptIn(): void { - const config = this._configurationService.getValue(TELEMETRY_SECTION_ID); + const config = this._configurationService?.getValue(TELEMETRY_SECTION_ID); this._userOptIn = config ? config.enableTelemetry : this._userOptIn; } diff --git a/src/vs/platform/theme/common/tokenClassificationRegistry.ts b/src/vs/platform/theme/common/tokenClassificationRegistry.ts index f2fd8eb4264..ab705dbcab3 100644 --- a/src/vs/platform/theme/common/tokenClassificationRegistry.ts +++ b/src/vs/platform/theme/common/tokenClassificationRegistry.ts @@ -312,9 +312,9 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry { }; constructor() { - this.tokenTypeById = {}; - this.tokenModifierById = {}; - this.typeHierarchy = {}; + this.tokenTypeById = Object.create(null); + this.tokenModifierById = Object.create(null); + this.typeHierarchy = Object.create(null); } public registerTokenType(id: string, description: string, superType?: string, deprecationMessage?: string): void { @@ -331,7 +331,7 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry { const stylingSchemeEntry = getStylingSchemeEntry(description, deprecationMessage); this.tokenStylingSchema.properties[id] = stylingSchemeEntry; - this.typeHierarchy = {}; + this.typeHierarchy = Object.create(null); } public registerTokenModifier(id: string, description: string, deprecationMessage?: string): void { @@ -398,7 +398,7 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry { public deregisterTokenType(id: string): void { delete this.tokenTypeById[id]; delete this.tokenStylingSchema.properties[id]; - this.typeHierarchy = {}; + this.typeHierarchy = Object.create(null); } public deregisterTokenModifier(id: string): void { diff --git a/src/vs/platform/theme/electron-main/themeMainService.ts b/src/vs/platform/theme/electron-main/themeMainService.ts index 0d16ddb9002..7bbacdb3d52 100644 --- a/src/vs/platform/theme/electron-main/themeMainService.ts +++ b/src/vs/platform/theme/electron-main/themeMainService.ts @@ -42,14 +42,14 @@ export class ThemeMainService implements IThemeMainService { } getBackgroundColor(): string { - if (isWindows && nativeTheme.shouldUseInvertedColorScheme) { + if ((isWindows || isMacintosh) && nativeTheme.shouldUseInvertedColorScheme) { return DEFAULT_BG_HC_BLACK; } let background = this.stateService.getItem(THEME_BG_STORAGE_KEY, null); if (!background) { let baseTheme: string; - if (isWindows && nativeTheme.shouldUseInvertedColorScheme) { + if ((isWindows || isMacintosh) && nativeTheme.shouldUseInvertedColorScheme) { baseTheme = 'hc-black'; } else { baseTheme = this.stateService.getItem(THEME_STORAGE_KEY, 'vs-dark').split(' ')[0]; diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 3457eabe31e..4e654f548e6 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -53,6 +53,10 @@ function isSyncData(thing: any): thing is ISyncData { return false; } +function getLastSyncResourceUri(syncResource: SyncResource, environmentService: IEnvironmentService): URI { + return joinPath(environmentService.userDataSyncHome, syncResource, `lastSync${syncResource}.json`); +} + export interface IResourcePreview { readonly remoteResource: URI; @@ -133,7 +137,7 @@ export abstract class AbstractSynchroniser extends Disposable { this.syncResourceLogLabel = uppercaseFirstLetter(this.resource); this.syncFolder = joinPath(environmentService.userDataSyncHome, resource); this.syncPreviewFolder = joinPath(this.syncFolder, PREVIEW_DIR_NAME); - this.lastSyncResource = joinPath(this.syncFolder, `lastSync${this.resource}.json`); + this.lastSyncResource = getLastSyncResourceUri(resource, environmentService); this.currentMachineIdPromise = getServiceMachineId(environmentService, fileService, storageService); } @@ -324,6 +328,7 @@ export abstract class AbstractSynchroniser extends Disposable { this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize ${this.syncResourceLogLabel} as there is a new local version available. Synchronizing again...`); return this.performSync(remoteUserData, lastSyncUserData, apply); + case UserDataSyncErrorCode.Conflict: case UserDataSyncErrorCode.PreconditionFailed: // Rejected as there is a new remote version. Syncing again... this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize as there is a new remote version available. Synchronizing again...`); @@ -796,3 +801,62 @@ export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroni } } + +export abstract class AbstractInitializer { + + private readonly lastSyncResource: URI; + + constructor( + readonly resource: SyncResource, + @IEnvironmentService protected readonly environmentService: IEnvironmentService, + @IUserDataSyncLogService protected readonly logService: IUserDataSyncLogService, + @IFileService protected readonly fileService: IFileService, + ) { + this.lastSyncResource = getLastSyncResourceUri(this.resource, environmentService); + } + + async initialize({ ref, content }: IUserData): Promise { + if (!content) { + this.logService.info('Remote content does not exist.', this.resource); + return; + } + + const syncData = this.parseSyncData(content); + if (!syncData) { + return; + } + + const isPreviouslySynced = await this.fileService.exists(this.lastSyncResource); + if (isPreviouslySynced) { + this.logService.info('Remote content does not exist.', this.resource); + return; + } + + try { + await this.doInitialize({ ref, syncData }); + } catch (error) { + this.logService.error(error); + } + } + + private parseSyncData(content: string): ISyncData | undefined { + try { + const syncData: ISyncData = JSON.parse(content); + if (isSyncData(syncData)) { + return syncData; + } + } catch (error) { + this.logService.error(error); + } + this.logService.info('Cannot parse sync data as it is not compatible with the current version.', this.resource); + return undefined; + } + + protected async updateLastSyncUserData(lastSyncRemoteUserData: IRemoteUserData, additionalProps: IStringDictionary = {}): Promise { + const lastSyncUserData: IUserData = { ref: lastSyncRemoteUserData.ref, content: lastSyncRemoteUserData.syncData ? JSON.stringify(lastSyncRemoteUserData.syncData) : null, ...additionalProps }; + await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData))); + } + + protected abstract doInitialize(remoteUserData: IRemoteUserData): Promise; + +} diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index d0c714df48b..0fe85a4a9e8 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -15,7 +15,7 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import { IFileService } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { merge, getIgnoredExtensions } from 'vs/platform/userDataSync/common/extensionsMerge'; -import { AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractInitializer, AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; import { joinPath, dirname, basename, isEqual } from 'vs/base/common/resources'; @@ -42,6 +42,34 @@ interface ILastSyncUserData extends IRemoteUserData { skippedExtensions: ISyncExtension[] | undefined; } +async function parseAndMigrateExtensions(syncData: ISyncData, extensionManagementService: IExtensionManagementService): Promise { + const extensions = JSON.parse(syncData.content); + if (syncData.version === 1 + || syncData.version === 2 + ) { + const systemExtensions = await extensionManagementService.getInstalled(ExtensionType.System); + for (const extension of extensions) { + // #region Migration from v1 (enabled -> disabled) + if (syncData.version === 1) { + if ((extension).enabled === false) { + extension.disabled = true; + } + delete (extension).enabled; + } + // #endregion + + // #region Migration from v2 (set installed property on extension) + if (syncData.version === 2) { + if (systemExtensions.every(installed => !areSameExtensions(installed.identifier, extension.identifier))) { + extension.installed = true; + } + } + // #endregion + } + } + return extensions; +} + export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/extensions.json` }); @@ -84,9 +112,9 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { - const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null; + const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await parseAndMigrateExtensions(remoteUserData.syncData, this.extensionManagementService) : null; const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : []; - const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? await this.parseAndMigrateExtensions(lastSyncUserData.syncData!) : null; + const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? await parseAndMigrateExtensions(lastSyncUserData.syncData!, this.extensionManagementService) : null; const installedExtensions = await this.extensionManagementService.getInstalled(); const localExtensions = this.getLocalExtensions(installedExtensions); @@ -385,34 +413,6 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse return newSkippedExtensions; } - private async parseAndMigrateExtensions(syncData: ISyncData): Promise { - const extensions = this.parseExtensions(syncData); - if (syncData.version === 1 - || syncData.version === 2 - ) { - const systemExtensions = await this.extensionManagementService.getInstalled(ExtensionType.System); - for (const extension of extensions) { - // #region Migration from v1 (enabled -> disabled) - if (syncData.version === 1) { - if ((extension).enabled === false) { - extension.disabled = true; - } - delete (extension).enabled; - } - // #endregion - - // #region Migration from v2 (set installed property on extension) - if (syncData.version === 2) { - if (systemExtensions.every(installed => !areSameExtensions(installed.identifier, extension.identifier))) { - extension.installed = true; - } - } - // #endregion - } - } - return extensions; - } - private parseExtensions(syncData: ISyncData): ISyncExtension[] { return JSON.parse(syncData.content); } @@ -433,3 +433,68 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } } + +export class ExtensionsInitializer extends AbstractInitializer { + + constructor( + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, + @IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService, + @IFileService fileService: IFileService, + @IEnvironmentService environmentService: IEnvironmentService, + @IUserDataSyncLogService logService: IUserDataSyncLogService, + ) { + super(SyncResource.Extensions, environmentService, logService, fileService); + } + + async doInitialize(remoteUserData: IRemoteUserData): Promise { + const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await parseAndMigrateExtensions(remoteUserData.syncData, this.extensionManagementService) : null; + if (!remoteExtensions) { + this.logService.info('Skipping initializing extensions because remote extensions does not exist.'); + return; + } + + const installedExtensions = await this.extensionManagementService.getInstalled(); + const toInstall: { names: string[], uuids: string[] } = { names: [], uuids: [] }; + const toDisable: IExtensionIdentifier[] = []; + for (const extension of remoteExtensions) { + if (installedExtensions.some(i => areSameExtensions(i.identifier, extension.identifier))) { + if (extension.disabled) { + toDisable.push(extension.identifier); + } + } else { + if (extension.installed) { + if (extension.identifier.uuid) { + toInstall.uuids.push(extension.identifier.uuid); + } else { + toInstall.names.push(extension.identifier.id); + } + } + } + } + + if (toInstall.names.length || toInstall.uuids.length) { + const galleryExtensions = (await this.galleryService.query({ ids: toInstall.uuids, names: toInstall.names, pageSize: toInstall.uuids.length + toInstall.names.length }, CancellationToken.None)).firstPage; + for (const galleryExtension of galleryExtensions) { + try { + this.logService.trace(`Installing extension...`, galleryExtension.identifier.id); + await this.extensionManagementService.installFromGallery(galleryExtension); + this.logService.info(`Installed extension.`, galleryExtension.identifier.id); + } catch (error) { + this.logService.error(error); + } + } + } + + if (toDisable.length) { + for (const identifier of toDisable) { + this.logService.trace(`Enabling extension...`, identifier.id); + await this.extensionEnablementService.disableExtension(identifier); + this.logService.info(`Enabled extension`, identifier.id); + } + } + } + +} + + diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 5929719d952..ab8209b396b 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -16,7 +16,7 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { edit } from 'vs/platform/userDataSync/common/content'; import { merge } from 'vs/platform/userDataSync/common/globalStateMerge'; import { parse } from 'vs/base/common/json'; -import { AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractInitializer, AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { URI } from 'vs/base/common/uri'; @@ -341,3 +341,55 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs return [...this.storageKeysSyncRegistryService.storageKeys, ...argvProperties.map(argvProprety => ({ key: `${argvStoragePrefx}${argvProprety}`, version: 1 }))]; } } + +export class GlobalStateInitializer extends AbstractInitializer { + + constructor( + @IStorageService private readonly storageService: IStorageService, + @IFileService fileService: IFileService, + @IEnvironmentService environmentService: IEnvironmentService, + @IUserDataSyncLogService logService: IUserDataSyncLogService, + ) { + super(SyncResource.GlobalState, environmentService, logService, fileService); + } + + async doInitialize(remoteUserData: IRemoteUserData): Promise { + const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; + if (!remoteGlobalState) { + this.logService.info('Skipping initializing global state because remote global state does not exist.'); + return; + } + + const argv: IStringDictionary = {}; + const storage: IStringDictionary = {}; + for (const key of Object.keys(remoteGlobalState.storage)) { + if (key.startsWith(argvStoragePrefx)) { + argv[key.substring(argvStoragePrefx.length)] = remoteGlobalState.storage[key].value; + } else { + if (this.storageService.get(key, StorageScope.GLOBAL) === undefined) { + storage[key] = remoteGlobalState.storage[key].value; + } + } + } + + if (Object.keys(argv).length) { + let content = '{}'; + try { + const fileContent = await this.fileService.readFile(this.environmentService.argvResource); + content = fileContent.value.toString(); + } catch (error) { } + for (const argvProperty of Object.keys(argv)) { + content = edit(content, [argvProperty], argv[argvProperty], {}); + } + await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(content)); + } + + if (Object.keys(storage).length) { + for (const key of Object.keys(storage)) { + this.storageService.store(key, storage[key], StorageScope.GLOBAL); + } + } + } + +} + diff --git a/src/vs/platform/userDataSync/common/keybindingsMerge.ts b/src/vs/platform/userDataSync/common/keybindingsMerge.ts index 3f994050bc7..3cbd0faf84d 100644 --- a/src/vs/platform/userDataSync/common/keybindingsMerge.ts +++ b/src/vs/platform/userDataSync/common/keybindingsMerge.ts @@ -28,10 +28,14 @@ interface IMergeResult { conflicts: Set; } +export function parseKeybindings(content: string): IUserFriendlyKeybinding[] { + return parse(content) || []; +} + export async function merge(localContent: string, remoteContent: string, baseContent: string | null, formattingOptions: FormattingOptions, userDataSyncUtilService: IUserDataSyncUtilService): Promise<{ mergeContent: string, hasChanges: boolean, hasConflicts: boolean }> { - const local = parse(localContent); - const remote = parse(remoteContent); - const base = baseContent ? parse(baseContent) : null; + const local = parseKeybindings(localContent); + const remote = parseKeybindings(remoteContent); + const base = baseContent ? parseKeybindings(baseContent) : null; const userbindings: string[] = [...local, ...remote, ...(base || [])].map(keybinding => keybinding.key); const normalizedKeys = await userDataSyncUtilService.resolveUserBindings(userbindings); @@ -331,7 +335,7 @@ function addKeybindings(content: string, keybindings: IUserFriendlyKeybinding[], } function removeKeybindings(content: string, command: string, formattingOptions: FormattingOptions): string { - const keybindings = parse(content); + const keybindings = parseKeybindings(content); for (let index = keybindings.length - 1; index >= 0; index--) { if (keybindings[index].command === command || keybindings[index].command === `-${command}`) { content = contentUtil.edit(content, [index], undefined, formattingOptions); @@ -341,7 +345,7 @@ function removeKeybindings(content: string, command: string, formattingOptions: } function updateKeybindings(content: string, command: string, keybindings: IUserFriendlyKeybinding[], formattingOptions: FormattingOptions): string { - const allKeybindings = parse(content); + const allKeybindings = parseKeybindings(content); const location = findFirstIndex(allKeybindings, keybinding => keybinding.command === command || keybinding.command === `-${command}`); // Remove all entries with this command for (let index = allKeybindings.length - 1; index >= 0; index--) { diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index cfafec8c3ae..3d9816563da 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -18,11 +18,12 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { OS, OperatingSystem } from 'vs/base/common/platform'; import { isUndefined } from 'vs/base/common/types'; import { isNonEmptyArray } from 'vs/base/common/arrays'; -import { AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractInitializer, AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { VSBuffer } from 'vs/base/common/buffer'; interface ISyncContent { mac?: string; @@ -35,6 +36,21 @@ interface IKeybindingsResourcePreview extends IFileResourcePreview { previewResult: IMergeResult; } +export function getKeybindingsContentFromSyncContent(syncContent: string, platformSpecific: boolean): string | null { + const parsed = JSON.parse(syncContent); + if (!platformSpecific) { + return isUndefined(parsed.all) ? null : parsed.all; + } + switch (OS) { + case OperatingSystem.Macintosh: + return isUndefined(parsed.mac) ? null : parsed.mac; + case OperatingSystem.Linux: + return isUndefined(parsed.linux) ? null : parsed.linux; + case OperatingSystem.Windows: + return isUndefined(parsed.windows) ? null : parsed.windows; + } +} + export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser { /* Version 2: Change settings from `sync.${setting}` to `settingsSync.{setting}` */ @@ -209,7 +225,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem if (lastSyncUserData?.ref !== remoteUserData.ref) { this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized keybindings...`); - const lastSyncContent = content !== null ? this.toSyncContent(content, null) : null; + const lastSyncContent = content !== null ? this.toSyncContent(content, null) : remoteUserData.syncData?.content; await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: lastSyncContent ? { @@ -266,20 +282,9 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem return null; } - getKeybindingsContentFromSyncContent(syncContent: string): string | null { + private getKeybindingsContentFromSyncContent(syncContent: string): string | null { try { - const parsed = JSON.parse(syncContent); - if (!this.syncKeybindingsPerPlatform()) { - return isUndefined(parsed.all) ? null : parsed.all; - } - switch (OS) { - case OperatingSystem.Macintosh: - return isUndefined(parsed.mac) ? null : parsed.mac; - case OperatingSystem.Linux: - return isUndefined(parsed.linux) ? null : parsed.linux; - case OperatingSystem.Windows: - return isUndefined(parsed.windows) ? null : parsed.windows; - } + return getKeybindingsContentFromSyncContent(syncContent, this.syncKeybindingsPerPlatform()); } catch (e) { this.logService.error(e); return null; @@ -325,3 +330,52 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem } } + +export class KeybindingsInitializer extends AbstractInitializer { + + constructor( + @IFileService fileService: IFileService, + @IEnvironmentService environmentService: IEnvironmentService, + @IUserDataSyncLogService logService: IUserDataSyncLogService, + ) { + super(SyncResource.Keybindings, environmentService, logService, fileService); + } + + async doInitialize(remoteUserData: IRemoteUserData): Promise { + const keybindingsContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null; + if (!keybindingsContent) { + this.logService.info('Skipping initializing keybindings because remote keybindings does not exist.'); + return; + } + + const isEmpty = await this.isEmpty(); + if (!isEmpty) { + this.logService.info('Skipping initializing keybindings because local keybindings exist.'); + return; + } + + await this.fileService.writeFile(this.environmentService.keybindingsResource, VSBuffer.fromString(keybindingsContent)); + + await this.updateLastSyncUserData(remoteUserData); + } + + private async isEmpty(): Promise { + try { + const fileContent = await this.fileService.readFile(this.environmentService.settingsResource); + const keybindings = parse(fileContent.value.toString()); + return !isNonEmptyArray(keybindings); + } catch (error) { + return (error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND; + } + } + + private getKeybindingsContentFromSyncContent(syncContent: string): string | null { + try { + return getKeybindingsContentFromSyncContent(syncContent, true); + } catch (e) { + this.logService.error(e); + return null; + } + } + +} diff --git a/src/vs/platform/userDataSync/common/settingsMerge.ts b/src/vs/platform/userDataSync/common/settingsMerge.ts index ad6800c6cbb..f4333ac84aa 100644 --- a/src/vs/platform/userDataSync/common/settingsMerge.ts +++ b/src/vs/platform/userDataSync/common/settingsMerge.ts @@ -275,8 +275,11 @@ export function areSame(localContent: string, remoteContent: string, ignoredSett } export function isEmpty(content: string): boolean { - const nodes = parseSettings(content); - return nodes.length === 0; + if (content) { + const nodes = parseSettings(content); + return nodes.length === 0; + } + return true; } function compare(from: IStringDictionary | null, to: IStringDictionary, ignored: Set): { added: Set, removed: Set, updated: Set } { diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index cbba5c25c3d..f1734e5e838 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -17,7 +17,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { CancellationToken } from 'vs/base/common/cancellation'; import { updateIgnoredSettings, merge, getIgnoredSettings, isEmpty } from 'vs/platform/userDataSync/common/settingsMerge'; import { edit } from 'vs/platform/userDataSync/common/content'; -import { AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractInitializer, AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -40,6 +40,11 @@ function isSettingsSyncContent(thing: any): thing is ISettingsSyncContent { && Object.keys(thing).length === 1; } +export function parseSettingsSyncContent(syncContent: string): ISettingsSyncContent { + const parsed = JSON.parse(syncContent); + return isSettingsSyncContent(parsed) ? parsed : /* migrate */ { settings: syncContent }; +} + export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser { /* Version 2: Change settings from `sync.${setting}` to `settingsSync.{setting}` */ @@ -281,10 +286,9 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement return remoteUserData.syncData ? this.parseSettingsSyncContent(remoteUserData.syncData.content) : null; } - parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null { + private parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null { try { - const parsed = JSON.parse(syncContent); - return isSettingsSyncContent(parsed) ? parsed : /* migrate */ { settings: syncContent }; + return parseSettingsSyncContent(syncContent); } catch (e) { this.logService.error(e); } @@ -350,6 +354,54 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement } } +export class SettingsInitializer extends AbstractInitializer { + + constructor( + @IFileService fileService: IFileService, + @IEnvironmentService environmentService: IEnvironmentService, + @IUserDataSyncLogService logService: IUserDataSyncLogService, + ) { + super(SyncResource.Settings, environmentService, logService, fileService); + } + + async doInitialize(remoteUserData: IRemoteUserData): Promise { + const settingsSyncContent = remoteUserData.syncData ? this.parseSettingsSyncContent(remoteUserData.syncData.content) : null; + if (!settingsSyncContent) { + this.logService.info('Skipping initializing settings because remote settings does not exist.'); + return; + } + + const isEmpty = await this.isEmpty(); + if (!isEmpty) { + this.logService.info('Skipping initializing settings because local settings exist.'); + return; + } + + await this.fileService.writeFile(this.environmentService.settingsResource, VSBuffer.fromString(settingsSyncContent.settings)); + + await this.updateLastSyncUserData(remoteUserData); + } + + private async isEmpty(): Promise { + try { + const fileContent = await this.fileService.readFile(this.environmentService.settingsResource); + return isEmpty(fileContent.value.toString().trim()); + } catch (error) { + return (error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND; + } + } + + private parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null { + try { + return parseSettingsSyncContent(syncContent); + } catch (e) { + this.logService.error(e); + } + return null; + } + +} + function isSyncData(thing: any): thing is ISyncData { if (thing && (thing.version !== undefined && typeof thing.version === 'number') diff --git a/src/vs/platform/userDataSync/common/snippetsSync.ts b/src/vs/platform/userDataSync/common/snippetsSync.ts index f00332c395a..c5b1029bd14 100644 --- a/src/vs/platform/userDataSync/common/snippetsSync.ts +++ b/src/vs/platform/userDataSync/common/snippetsSync.ts @@ -10,7 +10,7 @@ import { import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService, FileChangesEvent, IFileStat, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { AbstractSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractInitializer, AbstractSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IStringDictionary } from 'vs/base/common/collections'; import { URI } from 'vs/base/common/uri'; @@ -499,3 +499,49 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD return snippets; } } + +export class SnippetsInitializer extends AbstractInitializer { + + constructor( + @IFileService fileService: IFileService, + @IEnvironmentService environmentService: IEnvironmentService, + @IUserDataSyncLogService logService: IUserDataSyncLogService, + ) { + super(SyncResource.Snippets, environmentService, logService, fileService); + } + + async doInitialize(remoteUserData: IRemoteUserData): Promise { + const remoteSnippets: IStringDictionary | null = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; + if (!remoteSnippets) { + this.logService.info('Skipping initializing snippets because remote snippets does not exist.'); + return; + } + + const isEmpty = await this.isEmpty(); + if (!isEmpty) { + this.logService.info('Skipping initializing snippets because local snippets exist.'); + return; + } + + for (const key of Object.keys(remoteSnippets)) { + const content = remoteSnippets[key]; + if (content) { + const resource = joinPath(this.environmentService.snippetsHome, key); + await this.fileService.createFile(resource, VSBuffer.fromString(content)); + this.logService.info('Created snippet', basename(resource)); + } + } + + await this.updateLastSyncUserData(remoteUserData); + } + + private async isEmpty(): Promise { + try { + const stat = await this.fileService.resolve(this.environmentService.snippetsHome); + return !stat.children?.length; + } catch (error) { + return (error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND; + } + } + +} diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index 097be01f51d..9574d24e419 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -131,8 +131,8 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i this._register(userDataSyncAccountService.onDidChangeAccount(() => this.updateAutoSync())); this._register(userDataSyncStoreService.onDidChangeDonotMakeRequestsUntil(() => this.updateAutoSync())); - this._register(Event.debounce(userDataSyncService.onDidChangeLocal, (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, false))); - this._register(Event.filter(this.userDataSyncResourceEnablementService.onDidChangeResourceEnablement, ([, enabled]) => enabled)(() => this.triggerSync(['resourceEnablement'], false))); + this._register(Event.debounce(userDataSyncService.onDidChangeLocal, (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, false, false))); + this._register(Event.filter(this.userDataSyncResourceEnablementService.onDidChangeResourceEnablement, ([, enabled]) => enabled)(() => this.triggerSync(['resourceEnablement'], false, false))); } } @@ -320,7 +320,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i } private sources: string[] = []; - async triggerSync(sources: string[], skipIfSyncedRecently: boolean): Promise { + async triggerSync(sources: string[], skipIfSyncedRecently: boolean, disableCache: boolean): Promise { if (this.autoSync.value === undefined) { return this.syncTriggerDelayer.cancel(); } @@ -337,7 +337,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i this.telemetryService.publicLog2<{ sources: string[] }, AutoSyncClassification>('sync/triggered', { sources: this.sources }); this.sources = []; if (this.autoSync.value) { - await this.autoSync.value.sync('Activity'); + await this.autoSync.value.sync('Activity', disableCache); } }, this.successiveFailures ? this.getSyncTriggerDelayTime() * 1 * Math.min(Math.pow(2, this.successiveFailures), 60) /* Delay exponentially until max 1 minute */ @@ -393,14 +393,14 @@ class AutoSync extends Disposable { this.logService.info('Auto Sync: Stopped'); })); this.logService.info('Auto Sync: Started'); - this.sync(AutoSync.INTERVAL_SYNCING); + this.sync(AutoSync.INTERVAL_SYNCING, false); } private waitUntilNextIntervalAndSync(): void { - this.intervalHandler.value = disposableTimeout(() => this.sync(AutoSync.INTERVAL_SYNCING), this.interval); + this.intervalHandler.value = disposableTimeout(() => this.sync(AutoSync.INTERVAL_SYNCING, false), this.interval); } - sync(reason: string): Promise { + sync(reason: string, disableCache: boolean): Promise { const syncPromise = createCancelablePromise(async token => { if (this.syncPromise) { try { @@ -414,7 +414,7 @@ class AutoSync extends Disposable { } } } - return this.doSync(reason, token); + return this.doSync(reason, disableCache, token); }); this.syncPromise = syncPromise; this.syncPromise.finally(() => this.syncPromise = undefined); @@ -435,12 +435,12 @@ class AutoSync extends Disposable { !isEqual(current.stableUrl, previous.stableUrl)); } - private async doSync(reason: string, token: CancellationToken): Promise { + private async doSync(reason: string, disableCache: boolean, token: CancellationToken): Promise { this.logService.info(`Auto Sync: Triggered by ${reason}`); this._onDidStartSync.fire(); let error: Error | undefined; try { - this.syncTask = await this.userDataSyncService.createSyncTask(); + this.syncTask = await this.userDataSyncService.createSyncTask(disableCache); if (token.isCancellationRequested) { return; } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index dbc24ef949e..2381e7e7345 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -164,10 +164,7 @@ export interface IUserDataSyncStoreManagementService { getPreviousUserDataSyncStore(): Promise; } -export const IUserDataSyncStoreService = createDecorator('IUserDataSyncStoreService'); -export interface IUserDataSyncStoreService { - readonly _serviceBrand: undefined; - +export interface IUserDataSyncStoreClient { readonly onDidChangeDonotMakeRequestsUntil: Event; readonly donotMakeRequestsUntil: Date | undefined; @@ -186,6 +183,11 @@ export interface IUserDataSyncStoreService { resolveContent(resource: ServerResource, ref: string): Promise; } +export const IUserDataSyncStoreService = createDecorator('IUserDataSyncStoreService'); +export interface IUserDataSyncStoreService extends IUserDataSyncStoreClient { + readonly _serviceBrand: undefined; +} + export const IUserDataSyncBackupStoreService = createDecorator('IUserDataSyncBackupStoreService'); export interface IUserDataSyncBackupStoreService { readonly _serviceBrand: undefined; @@ -208,6 +210,7 @@ export const HEADER_EXECUTION_ID = 'X-Execution-Id'; export enum UserDataSyncErrorCode { // Client Errors (>= 400 ) Unauthorized = 'Unauthorized', /* 401 */ + Conflict = 'Conflict', /* 409 */ Gone = 'Gone', /* 410 */ PreconditionFailed = 'PreconditionFailed', /* 412 */ TooLarge = 'TooLarge', /* 413 */ @@ -434,7 +437,7 @@ export interface IUserDataSyncService { readonly onDidResetRemote: Event; readonly onDidResetLocal: Event; - createSyncTask(): Promise; + createSyncTask(disableCache?: boolean): Promise; createManualSyncTask(): Promise; replace(uri: URI): Promise; @@ -462,7 +465,7 @@ export interface IUserDataAutoSyncService { canToggleEnablement(): boolean; turnOn(): Promise; turnOff(everywhere: boolean): Promise; - triggerSync(sources: string[], hasToLimitSync: boolean): Promise; + triggerSync(sources: string[], hasToLimitSync: boolean, disableCache: boolean): Promise; } export const IUserDataSyncUtilService = createDecorator('IUserDataSyncUtilService'); diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 8d6296e8051..08a8243bb42 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -128,7 +128,7 @@ export class UserDataAutoSyncChannel implements IServerChannel { call(context: any, command: string, args?: any): Promise { switch (command) { - case 'triggerSync': return this.service.triggerSync(args[0], args[1]); + case 'triggerSync': return this.service.triggerSync(args[0], args[1], args[2]); case 'turnOn': return this.service.turnOn(); case 'turnOff': return this.service.turnOff(args[0]); } diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index f9c446c2124..54d42878bf5 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -106,13 +106,17 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeLocal, () => s.resource))); } - async createSyncTask(): Promise { + async createSyncTask(disableCache?: boolean): Promise { await this.checkEnablement(); const executionId = generateUuid(); let manifest: IUserDataManifest | null; try { - manifest = await this.userDataSyncStoreService.manifest(createSyncHeaders(executionId)); + const syncHeaders = createSyncHeaders(executionId); + if (disableCache) { + syncHeaders['Cache-Control'] = 'no-cache'; + } + manifest = await this.userDataSyncStoreService.manifest(syncHeaders); } catch (error) { error = UserDataSyncError.toUserDataSyncError(error); this.telemetryService.publicLog2<{ code: string, service: string, resource?: string, executionId?: string }, SyncErrorClassification>('sync/error', { code: error.code, resource: error.resource, executionId, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() }); diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index d17ec3b39fc..0d9489ed8e9 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, } from 'vs/base/common/lifecycle'; -import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle, HEADER_OPERATION_ID, HEADER_EXECUTION_ID, CONFIGURATION_SYNC_STORE_KEY, IAuthenticationProvider, IUserDataSyncStoreManagementService, UserDataSyncStoreType } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle, HEADER_OPERATION_ID, HEADER_EXECUTION_ID, CONFIGURATION_SYNC_STORE_KEY, IAuthenticationProvider, IUserDataSyncStoreManagementService, UserDataSyncStoreType, IUserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSync'; import { IRequestService, asText, isSuccess as isSuccessContext, asJson } from 'vs/platform/request/common/request'; import { joinPath, relativePath } from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -125,9 +125,7 @@ export class UserDataSyncStoreManagementService extends AbstractUserDataSyncStor } } -export class UserDataSyncStoreService extends Disposable implements IUserDataSyncStoreService { - - _serviceBrand: any; +export class UserDataSyncStoreClient extends Disposable implements IUserDataSyncStoreClient { private readonly userDataSyncStoreUrl: URI | undefined; @@ -147,16 +145,16 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn readonly onDidChangeDonotMakeRequestsUntil = this._onDidChangeDonotMakeRequestsUntil.event; constructor( + userDataSyncStoreUrl: URI | undefined, @IProductService productService: IProductService, @IRequestService private readonly requestService: IRequestService, - @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @IEnvironmentService environmentService: IEnvironmentService, @IFileService fileService: IFileService, @IStorageService private readonly storageService: IStorageService, ) { super(); - this.userDataSyncStoreUrl = this.userDataSyncStoreManagementService.userDataSyncStore ? joinPath(this.userDataSyncStoreManagementService.userDataSyncStore.url, 'v1') : undefined; + this.userDataSyncStoreUrl = userDataSyncStoreUrl ? joinPath(userDataSyncStoreUrl, 'v1') : undefined; this.commonHeadersPromise = getServiceMachineId(environmentService, fileService, storageService) .then(uuid => { const headers: IHeaders = { @@ -395,12 +393,16 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn this._onTokenSucceed.fire(); + if (context.res.statusCode === 409) { + throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Conflict (409). There is new data for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.Conflict, operationId); + } + if (context.res.statusCode === 410) { throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because the requested resource is not longer available (410).`, UserDataSyncErrorCode.Gone, operationId); } if (context.res.statusCode === 412) { - throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.PreconditionFailed, operationId); + throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.PreconditionFailed, operationId); } if (context.res.statusCode === 413) { @@ -444,6 +446,23 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } +export class UserDataSyncStoreService extends UserDataSyncStoreClient implements IUserDataSyncStoreService { + + _serviceBrand: any; + + constructor( + @IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, + @IProductService productService: IProductService, + @IRequestService requestService: IRequestService, + @IUserDataSyncLogService logService: IUserDataSyncLogService, + @IEnvironmentService environmentService: IEnvironmentService, + @IFileService fileService: IFileService, + @IStorageService storageService: IStorageService, + ) { + super(userDataSyncStoreManagementService.userDataSyncStore?.url, productService, requestService, logService, environmentService, fileService, storageService); + } +} + export class RequestsSession { private requests: string[] = []; @@ -463,7 +482,7 @@ export class RequestsSession { if (this.requests.length >= this.limit) { this.logService.info('Too many requests', ...this.requests); - throw new UserDataSyncStoreError(`Too many requests. Allowed only ${this.limit} requests in ${this.interval / (1000 * 60)} minutes.`, UserDataSyncErrorCode.LocalTooManyRequests, undefined); + throw new UserDataSyncStoreError(`Too many requests. Only ${this.limit} requests allowed in ${this.interval / (1000 * 60)} minutes.`, UserDataSyncErrorCode.LocalTooManyRequests, undefined); } this.startTime = this.startTime || new Date(); diff --git a/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts index 81483659e9b..44ed7f9b7ea 100644 --- a/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts @@ -33,7 +33,7 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { this._register(Event.debounce(Event.any( Event.map(electronService.onWindowFocus, () => 'windowFocus'), Event.map(electronService.onWindowOpen, () => 'windowOpen'), - ), (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, true))); + ), (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, true, false))); } } diff --git a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts index 02b0154b627..9007dcdb544 100644 --- a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts @@ -10,7 +10,7 @@ import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { IFileService } from 'vs/platform/files/common/files'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync'; +import { getKeybindingsContentFromSyncContent, KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync'; import { VSBuffer } from 'vs/base/common/buffer'; suite('KeybindingsSync', () => { @@ -70,8 +70,8 @@ suite('KeybindingsSync', () => { const lastSyncUserData = await testObject.getLastSyncUserData(); const remoteUserData = await testObject.getRemoteUserData(null); - assert.equal(testObject.getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!), '[]'); - assert.equal(testObject.getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!), '[]'); + assert.equal(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), '[]'); + assert.equal(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), '[]'); assert.equal((await fileService.readFile(keybindingsResource)).value.toString(), ''); }); @@ -95,11 +95,75 @@ suite('KeybindingsSync', () => { const lastSyncUserData = await testObject.getLastSyncUserData(); const remoteUserData = await testObject.getRemoteUserData(null); - assert.equal(testObject.getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!), content); - assert.equal(testObject.getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!), content); + assert.equal(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), content); + assert.equal(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), content); assert.equal((await fileService.readFile(keybindingsResource)).value.toString(), content); }); + test('when keybindings file is empty with comment and remote has no changes', async () => { + const fileService = client.instantiationService.get(IFileService); + const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource; + const expectedContent = '// Empty Keybindings'; + await fileService.writeFile(keybindingsResource, VSBuffer.fromString(expectedContent)); + + await testObject.sync(await client.manifest()); + + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.equal(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), expectedContent); + assert.equal(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), expectedContent); + assert.equal((await fileService.readFile(keybindingsResource)).value.toString(), expectedContent); + }); + + test('when keybindings file is empty and remote has keybindings', async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const content = JSON.stringify([ + { + 'key': 'shift+cmd+w', + 'command': 'workbench.action.closeAllEditors', + } + ]); + await client2.instantiationService.get(IFileService).writeFile(client2.instantiationService.get(IEnvironmentService).keybindingsResource, VSBuffer.fromString(content)); + await client2.sync(); + + const fileService = client.instantiationService.get(IFileService); + const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource; + await fileService.writeFile(keybindingsResource, VSBuffer.fromString('// Empty Keybindings')); + + await testObject.sync(await client.manifest()); + + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.equal(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), content); + assert.equal(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), content); + assert.equal((await fileService.readFile(keybindingsResource)).value.toString(), content); + }); + + test('when keybindings file is empty and remote has empty array', async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const content = + `// Place your key bindings in this file to override the defaults +[ +]`; + await client2.instantiationService.get(IFileService).writeFile(client2.instantiationService.get(IEnvironmentService).keybindingsResource, VSBuffer.fromString(content)); + await client2.sync(); + + const fileService = client.instantiationService.get(IFileService); + const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource; + const expectedLocalContent = '// Empty Keybindings'; + await fileService.writeFile(keybindingsResource, VSBuffer.fromString(expectedLocalContent)); + + await testObject.sync(await client.manifest()); + + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.equal(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), content); + assert.equal(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), content); + assert.equal((await fileService.readFile(keybindingsResource)).value.toString(), expectedLocalContent); + }); + test('when keybindings file is created after first sync', async () => { const fileService = client.instantiationService.get(IFileService); const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource; @@ -119,7 +183,7 @@ suite('KeybindingsSync', () => { const remoteUserData = await testObject.getRemoteUserData(null); assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); - assert.equal(testObject.getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!), '[]'); + assert.equal(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), '[]'); }); test('test apply remote when keybindings file does not exist', async () => { diff --git a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts index 6117c2b55e2..e83d8a6aa73 100644 --- a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, ISyncData, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { SettingsSynchroniser, ISettingsSyncContent } from 'vs/platform/userDataSync/common/settingsSync'; +import { SettingsSynchroniser, ISettingsSyncContent, parseSettingsSyncContent } from 'vs/platform/userDataSync/common/settingsSync'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { IFileService } from 'vs/platform/files/common/files'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -88,8 +88,8 @@ suite('SettingsSync - Auto', () => { const lastSyncUserData = await testObject.getLastSyncUserData(); const remoteUserData = await testObject.getRemoteUserData(null); - assert.equal(testObject.parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, '{}'); - assert.equal(testObject.parseSettingsSyncContent(remoteUserData!.syncData!.content!)?.settings, '{}'); + assert.equal(parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, '{}'); + assert.equal(parseSettingsSyncContent(remoteUserData!.syncData!.content!)?.settings, '{}'); assert.equal((await fileService.readFile(settingsResource)).value.toString(), ''); }); @@ -129,8 +129,8 @@ suite('SettingsSync - Auto', () => { const lastSyncUserData = await testObject.getLastSyncUserData(); const remoteUserData = await testObject.getRemoteUserData(null); - assert.equal(testObject.parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, content); - assert.equal(testObject.parseSettingsSyncContent(remoteUserData!.syncData!.content!)?.settings, content); + assert.equal(parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, content); + assert.equal(parseSettingsSyncContent(remoteUserData!.syncData!.content!)?.settings, content); assert.equal((await fileService.readFile(settingsResource)).value.toString(), content); }); @@ -154,7 +154,7 @@ suite('SettingsSync - Auto', () => { const remoteUserData = await testObject.getRemoteUserData(null); assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); - assert.equal(testObject.parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, '{}'); + assert.equal(parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, '{}'); }); test('sync for first time to the server', async () => { diff --git a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts index 310c69dd6cf..49df9ea7e0f 100644 --- a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts @@ -20,7 +20,7 @@ class TestUserDataAutoSyncService extends UserDataAutoSyncService { protected getSyncTriggerDelayTime(): number { return 50; } sync(): Promise { - return this.triggerSync(['sync'], false); + return this.triggerSync(['sync'], false, false); } } @@ -43,7 +43,7 @@ suite('UserDataAutoSyncService', () => { const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); // Trigger auto sync with settings change - await testObject.triggerSync([SyncResource.Settings], false); + await testObject.triggerSync([SyncResource.Settings], false, false); // Filter out machine requests const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`)); @@ -66,7 +66,7 @@ suite('UserDataAutoSyncService', () => { // Trigger auto sync with settings change multiple times for (let counter = 0; counter < 2; counter++) { - await testObject.triggerSync([SyncResource.Settings], false); + await testObject.triggerSync([SyncResource.Settings], false, false); } // Filter out machine requests @@ -91,7 +91,7 @@ suite('UserDataAutoSyncService', () => { const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); // Trigger auto sync with window focus once - await testObject.triggerSync(['windowFocus'], true); + await testObject.triggerSync(['windowFocus'], true, false); // Filter out machine requests const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`)); @@ -114,7 +114,7 @@ suite('UserDataAutoSyncService', () => { // Trigger auto sync with window focus multiple times for (let counter = 0; counter < 2; counter++) { - await testObject.triggerSync(['windowFocus'], true); + await testObject.triggerSync(['windowFocus'], true, false); } // Filter out machine requests @@ -401,4 +401,28 @@ suite('UserDataAutoSyncService', () => { assert.deepEqual(target.requests, []); }); + test('test cache control header with no cache is sent when triggered with disable cache option', async () => { + const target = new UserDataSyncTestServer(5, 1); + + // Set up and sync from the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + + await testObject.triggerSync(['some reason'], true, true); + assert.equal(target.requestsWithAllHeaders[0].headers!['Cache-Control'], 'no-cache'); + }); + + test('test cache control header is not sent when triggered without disable cache option', async () => { + const target = new UserDataSyncTestServer(5, 1); + + // Set up and sync from the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + + await testObject.triggerSync(['some reason'], true, false); + assert.equal(target.requestsWithAllHeaders[0].headers!['Cache-Control'], undefined); + }); + }); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 767fbbf6cb5..aa427813097 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -6,7 +6,7 @@ import { IRequestService } from 'vs/platform/request/common/request'; import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IUserData, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource, ServerResource, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource, ServerResource, IUserDataSyncStoreManagementService, registerConfiguration } from 'vs/platform/userDataSync/common/userDataSync'; import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { generateUuid } from 'vs/base/common/uuid'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; @@ -49,6 +49,7 @@ export class UserDataSyncClient extends Disposable { } async setUp(empty: boolean = false): Promise { + registerConfiguration(); const userRoamingDataHome = URI.file('userdata').with({ scheme: Schemas.inMemory }); const userDataSyncHome = joinPath(userRoamingDataHome, '.sync'); const environmentService = this.instantiationService.stub(IEnvironmentService, >{ diff --git a/src/vs/platform/webview/common/webviewManagerService.ts b/src/vs/platform/webview/common/webviewManagerService.ts index 56171e56fc0..8963865f3b6 100644 --- a/src/vs/platform/webview/common/webviewManagerService.ts +++ b/src/vs/platform/webview/common/webviewManagerService.ts @@ -14,7 +14,7 @@ export const IWebviewManagerService = createDecorator('w export interface IWebviewManagerService { _serviceBrand: unknown; - registerWebview(id: string, webContentsId: number | undefined, windowId: number, metadata: RegisterWebviewMetadata): Promise; + registerWebview(id: string, windowId: number, metadata: RegisterWebviewMetadata): Promise; unregisterWebview(id: string): Promise; updateWebviewMetadata(id: string, metadataDelta: Partial): Promise; diff --git a/src/vs/platform/webview/electron-main/webviewMainService.ts b/src/vs/platform/webview/electron-main/webviewMainService.ts index 0e29b1cbaff..c3b49724aba 100644 --- a/src/vs/platform/webview/electron-main/webviewMainService.ts +++ b/src/vs/platform/webview/electron-main/webviewMainService.ts @@ -33,7 +33,7 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer this.portMappingProvider = this._register(new WebviewPortMappingProvider(tunnelService)); } - public async registerWebview(id: string, webContentsId: number | undefined, windowId: number, metadata: RegisterWebviewMetadata): Promise { + public async registerWebview(id: string, windowId: number, metadata: RegisterWebviewMetadata): Promise { const extensionLocation = metadata.extensionLocation ? URI.from(metadata.extensionLocation) : undefined; this.protocolProvider.registerWebview(id, { @@ -43,7 +43,7 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer localResourceRoots: metadata.localResourceRoots.map(x => URI.from(x)) }); - this.portMappingProvider.registerWebview(id, webContentsId, { + this.portMappingProvider.registerWebview(id, { extensionLocation, mappings: metadata.portMappings, resolvedAuthority: metadata.remoteConnectionData, diff --git a/src/vs/platform/webview/electron-main/webviewPortMappingProvider.ts b/src/vs/platform/webview/electron-main/webviewPortMappingProvider.ts index 94dc3036ebc..da60a2bc507 100644 --- a/src/vs/platform/webview/electron-main/webviewPortMappingProvider.ts +++ b/src/vs/platform/webview/electron-main/webviewPortMappingProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { session } from 'electron'; +import { OnBeforeRequestListenerDetails, session } from 'electron'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IAddress } from 'vs/platform/remote/common/remoteAgentConnection'; @@ -11,6 +11,10 @@ import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { webviewPartitionId } from 'vs/platform/webview/common/resourceLoader'; import { IWebviewPortMapping, WebviewPortMappingManager } from 'vs/platform/webview/common/webviewPortMapping'; +interface OnBeforeRequestListenerDetails_Extended extends OnBeforeRequestListenerDetails { + readonly lastCommittedOrigin?: string; +} + interface PortMappingData { readonly extensionLocation: URI | undefined; readonly mappings: readonly IWebviewPortMapping[]; @@ -20,13 +24,10 @@ interface PortMappingData { export class WebviewPortMappingProvider extends Disposable { private readonly _webviewData = new Map(); - private _webContentsIdsToWebviewIds = new Map(); - constructor( @ITunnelService private readonly _tunnelService: ITunnelService, ) { @@ -40,12 +41,15 @@ export class WebviewPortMappingProvider extends Disposable { '*://127.0.0.1:*/*', '*://0.0.0.0:*/*', ] - }, async (details, callback) => { - const webviewId = details.webContentsId && this._webContentsIdsToWebviewIds.get(details.webContentsId); - if (!webviewId) { + }, async (details: OnBeforeRequestListenerDetails_Extended, callback) => { + let origin: URI; + try { + origin = URI.parse(details.lastCommittedOrigin!); + } catch { return callback({}); } + const webviewId = origin.authority; const entry = this._webviewData.get(webviewId); if (!entry) { return callback({}); @@ -56,16 +60,13 @@ export class WebviewPortMappingProvider extends Disposable { }); } - public async registerWebview(id: string, webContentsId: number | undefined, metadata: PortMappingData): Promise { + public async registerWebview(id: string, metadata: PortMappingData): Promise { const manager = new WebviewPortMappingManager( () => this._webviewData.get(id)?.metadata.extensionLocation, () => this._webviewData.get(id)?.metadata.mappings || [], this._tunnelService); - this._webviewData.set(id, { webContentsId, metadata, manager }); - if (typeof webContentsId === 'number') { - this._webContentsIdsToWebviewIds.set(webContentsId, id); - } + this._webviewData.set(id, { metadata, manager }); } public unregisterWebview(id: string): void { @@ -73,9 +74,6 @@ export class WebviewPortMappingProvider extends Disposable { if (existing) { existing.manager.dispose(); this._webviewData.delete(id); - if (typeof existing.webContentsId === 'number') { - this._webContentsIdsToWebviewIds.delete(existing.webContentsId); - } } } diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 4933ec468bb..5accb83f2ba 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -7,7 +7,6 @@ import { isMacintosh, isLinux, isWeb } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ThemeType } from 'vs/platform/theme/common/themeService'; import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; export interface IBaseOpenWindowsOptions { @@ -171,18 +170,46 @@ export interface IPathData { overrideId?: string; } +export interface IPathsToWaitFor extends IPathsToWaitForData { + paths: IPath[]; + waitMarkerFileUri: URI; +} + +interface IPathsToWaitForData { + paths: IPathData[]; + waitMarkerFileUri: UriComponents; +} + export interface IOpenFileRequest { filesToOpenOrCreate?: IPathData[]; filesToDiff?: IPathData[]; } +/** + * Additional context for the request on native only. + */ +export interface INativeOpenFileRequest extends IOpenFileRequest { + termProgram?: string; + filesToWait?: IPathsToWaitForData; +} + +export interface INativeRunActionInWindowRequest { + id: string; + from: 'menu' | 'touchbar' | 'mouse'; + args?: any[]; +} + +export interface INativeRunKeybindingInWindowRequest { + userSettingsLabel: string; +} + export interface IWindowConfiguration { sessionId: string; remoteAuthority?: string; highContrast?: boolean; - defaultThemeType?: ThemeType; + autoDetectHighContrast?: boolean; filesToOpenOrCreate?: IPath[]; filesToDiff?: IPath[]; diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 8df91e318a8..bf481595ade 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -106,7 +106,7 @@ export interface IWindowsMainService { openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): ICodeWindow[]; sendToFocused(channel: string, ...args: any[]): void; - sendToAll(channel: string, payload: any, windowIdsToIgnore?: number[]): void; + sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void; getLastActiveWindow(): ICodeWindow | undefined; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index e441c8c4bb0..1aec9526b10 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -19,8 +19,8 @@ import { screen, BrowserWindow, MessageBoxOptions, Display, app, nativeTheme } f import { ILifecycleMainService, UnloadReason, LifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; -import { IWindowSettings, IPath, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions, IAddFoldersRequest } from 'vs/platform/windows/common/windows'; -import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri, INativeWindowConfiguration, OpenContext, IPathsToWaitFor } from 'vs/platform/windows/node/window'; +import { IWindowSettings, IPath, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions, IAddFoldersRequest, IPathsToWaitFor } from 'vs/platform/windows/common/windows'; +import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri, INativeWindowConfiguration, OpenContext } from 'vs/platform/windows/node/window'; import { Emitter } from 'vs/base/common/event'; import product from 'vs/platform/product/common/product'; import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode, IOpenEmptyConfiguration } from 'vs/platform/windows/electron-main/windows'; @@ -40,6 +40,7 @@ import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; import { withNullAsUndefined } from 'vs/base/common/types'; import { isWindowsDriveLetter, toSlashes, parseLineAndColumnAware } from 'vs/base/common/extpath'; import { CharCode } from 'vs/base/common/charCode'; +import { getPathLabel } from 'vs/base/common/labels'; export interface IWindowState { workspace?: IWorkspaceIdentifier; @@ -212,8 +213,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic private registerListeners(): void { - // React to HC color scheme changes (Windows) - if (isWindows) { + // React to HC color scheme changes (Windows, macOS) + if (isWindows || isMacintosh) { nativeTheme.on('updated', () => { if (nativeTheme.shouldUseInvertedColorScheme || nativeTheme.shouldUseHighContrastColors) { this.sendToAll('vscode:enterHighContrast'); @@ -880,11 +881,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic let message, detail; if (uri.scheme === Schemas.file) { message = localize('pathNotExistTitle', "Path does not exist"); - detail = localize('pathNotExistDetail', "The path '{0}' does not seem to exist anymore on disk.", uri.fsPath); + detail = localize('pathNotExistDetail', "The path '{0}' does not seem to exist anymore on disk.", getPathLabel(uri.fsPath, this.environmentService)); } else { message = localize('uriInvalidTitle', "URI can not be opened"); detail = localize('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", uri.toString()); } + const options: MessageBoxOptions = { title: product.nameLong, type: 'info', @@ -1134,7 +1136,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic if (forceOpenWorkspaceAsFile) { return { fileUri: uri, remoteAuthority }; } - } else if (posix.extname(anyPath).length > 0) { + } else if (posix.basename(anyPath).indexOf('.') !== -1) { // file name starts with a dot or has an file extension return { fileUri: uri, remoteAuthority }; } } diff --git a/src/vs/platform/windows/node/window.ts b/src/vs/platform/windows/node/window.ts index 7d643a5963c..9adc39431df 100644 --- a/src/vs/platform/windows/node/window.ts +++ b/src/vs/platform/windows/node/window.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IWindowConfiguration, IPath, IOpenFileRequest, IPathData } from 'vs/platform/windows/common/windows'; -import { URI, UriComponents } from 'vs/base/common/uri'; +import { IWindowConfiguration, IPathsToWaitFor } from 'vs/platform/windows/common/windows'; +import { URI } from 'vs/base/common/uri'; import * as platform from 'vs/base/common/platform'; import * as extpath from 'vs/base/common/extpath'; import { IWorkspaceIdentifier, IResolvedWorkspace, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; @@ -34,16 +34,6 @@ export const enum OpenContext { API } -export interface IRunActionInWindowRequest { - id: string; - from: 'menu' | 'touchbar' | 'mouse'; - args?: any[]; -} - -export interface IRunKeybindingInWindowRequest { - userSettingsLabel: string; -} - export interface INativeWindowConfiguration extends IWindowConfiguration, ParsedArgs { mainPid: number; @@ -72,21 +62,6 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Parsed filesToWait?: IPathsToWaitFor; } -export interface INativeOpenFileRequest extends IOpenFileRequest { - termProgram?: string; - filesToWait?: IPathsToWaitForData; -} - -export interface IPathsToWaitFor extends IPathsToWaitForData { - paths: IPath[]; - waitMarkerFileUri: URI; -} - -export interface IPathsToWaitForData { - paths: IPathData[]; - waitMarkerFileUri: UriComponents; -} - export interface IWindowContext { openedWorkspace?: IWorkspaceIdentifier; openedFolderUri?: URI; diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index 0f2e82d0e85..c0c34d74f57 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -82,9 +82,9 @@ export interface IWorkspaceFoldersChangeEvent { export namespace IWorkspace { export function isIWorkspace(thing: unknown): thing is IWorkspace { - return thing && typeof thing === 'object' + return !!(thing && typeof thing === 'object' && typeof (thing as IWorkspace).id === 'string' - && Array.isArray((thing as IWorkspace).folders); + && Array.isArray((thing as IWorkspace).folders)); } } @@ -127,10 +127,10 @@ export interface IWorkspaceFolderData { export namespace IWorkspaceFolder { export function isIWorkspaceFolder(thing: unknown): thing is IWorkspaceFolder { - return thing && typeof thing === 'object' + return !!(thing && typeof thing === 'object' && URI.isUri((thing as IWorkspaceFolder).uri) && typeof (thing as IWorkspaceFolder).name === 'string' - && typeof (thing as IWorkspaceFolder).toResource === 'function'; + && typeof (thing as IWorkspaceFolder).toResource === 'function'); } } diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index ef711dc3855..07f2be17b28 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1112,7 +1112,7 @@ declare module 'vscode' { * isn't one of the main editors, e.g. an embedded editor, or when the editor * column is larger than three. */ - viewColumn?: ViewColumn; + readonly viewColumn?: ViewColumn; /** * Perform an edit on the document associated with this text editor. @@ -3086,6 +3086,8 @@ declare module 'vscode' { * @param uri Uri of the new file.. * @param options Defines if an existing file should be overwritten or be * ignored. When overwrite and ignoreIfExists are both set overwrite wins. + * When both are unset and when the file already exists then the edit cannot + * be applied successfully. * @param metadata Optional metadata for the entry. */ createFile(uri: Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean }, metadata?: WorkspaceEditEntryMetadata): void; @@ -5420,6 +5422,66 @@ declare module 'vscode' { dispose(): void; } + /** + * Provides information on a line in a terminal in order to provide links for it. + */ + export interface TerminalLinkContext { + /** + * This is the text from the unwrapped line in the terminal. + */ + line: string; + + /** + * The terminal the link belongs to. + */ + terminal: Terminal; + } + + /** + * A provider that enables detection and handling of links within terminals. + */ + export interface TerminalLinkProvider { + /** + * Provide terminal links for the given context. Note that this can be called multiple times + * even before previous calls resolve, make sure to not share global objects (eg. `RegExp`) + * that could have problems when asynchronous usage may overlap. + * @param context Information about what links are being provided for. + * @param token A cancellation token. + * @return A list of terminal links for the given line. + */ + provideTerminalLinks(context: TerminalLinkContext, token: CancellationToken): ProviderResult + + /** + * Handle an activated terminal link. + * @param link The link to handle. + */ + handleTerminalLink(link: T): ProviderResult; + } + + /** + * A link on a terminal line. + */ + export interface TerminalLink { + /** + * The start index of the link on [TerminalLinkContext.line](#TerminalLinkContext.line]. + */ + startIndex: number; + + /** + * The length of the link on [TerminalLinkContext.line](#TerminalLinkContext.line] + */ + length: number; + + /** + * The tooltip text when you hover over this link. + * + * If a tooltip is provided, is will be displayed in a string that includes instructions on + * how to trigger the link, such as `{0} (ctrl + click)`. The specific instructions vary + * depending on OS, user settings, and localization. + */ + tooltip?: string; + } + /** * In a remote window the extension kind describes if an extension * runs where the UI (window) runs or if an extension runs remotely. @@ -5564,11 +5626,28 @@ declare module 'vscode' { /** * Get the absolute path of a resource contained in the extension. * + * *Note* that an absolute uri can be constructed via [`Uri.joinPath`](#Uri.joinPath) and + * [`extensionUri`](#ExtensionContent.extensionUri), e.g. `vscode.Uri.joinPath(context.extensionUri, relativePath);` + * * @param relativePath A relative path to a resource contained in the extension. * @return The absolute path of the resource. */ asAbsolutePath(relativePath: string): string; + /** + * The uri of a workspace specific directory in which the extension + * can store private state. The directory might not exist and creation is + * up to the extension. However, the parent directory is guaranteed to be existent. + * The value is `undefined` when no workspace nor folder has been opened. + * + * Use [`workspaceState`](#ExtensionContext.workspaceState) or + * [`globalState`](#ExtensionContext.globalState) to store key value data. + * + * @see [`workspace.fs`](#FileSystem) for how to read and write files and folders from + * an uri. + */ + readonly storageUri: Uri | undefined; + /** * An absolute file path of a workspace specific directory in which the extension * can store private state. The directory might not exist on disk and creation is @@ -5576,22 +5655,50 @@ declare module 'vscode' { * * Use [`workspaceState`](#ExtensionContext.workspaceState) or * [`globalState`](#ExtensionContext.globalState) to store key value data. + * + * @deprecated Use [storagePath](#ExtensionContent.storageUri) instead. */ readonly storagePath: string | undefined; + /** + * The uri of a directory in which the extension can store global state. + * The directory might not exist on disk and creation is + * up to the extension. However, the parent directory is guaranteed to be existent. + * + * Use [`globalState`](#ExtensionContext.globalState) to store key value data. + * + * @see [`workspace.fs`](#FileSystem) for how to read and write files and folders from + * an uri. + */ + readonly globalStorageUri: Uri; + /** * An absolute file path in which the extension can store global state. * The directory might not exist on disk and creation is * up to the extension. However, the parent directory is guaranteed to be existent. * * Use [`globalState`](#ExtensionContext.globalState) to store key value data. + * + * @deprecated Use [globalStoragePath](#ExtensionContent.globalStorageUri) instead. */ readonly globalStoragePath: string; + /** + * The uri of a directory in which the extension can create log files. + * The directory might not exist on disk and creation is up to the extension. However, + * the parent directory is guaranteed to be existent. + * + * @see [`workspace.fs`](#FileSystem) for how to read and write files and folders from + * an uri. + */ + readonly logUri: Uri; + /** * An absolute file path of a directory in which the extension can create log files. * The directory might not exist on disk and creation is up to the extension. However, * the parent directory is guaranteed to be existent. + * + * @deprecated Use [logUri](#ExtensionContext.logUri) instead. */ readonly logPath: string; @@ -6021,9 +6128,10 @@ declare module 'vscode' { * [Pseudoterminal.close](#Pseudoterminal.close). When the task is complete fire * [Pseudoterminal.onDidClose](#Pseudoterminal.onDidClose). * @param process The [Pseudoterminal](#Pseudoterminal) to be used by the task to display output. - * @param callback The callback that will be called when the task is started by a user. + * @param callback The callback that will be called when the task is started by a user. Any ${} style variables that + * were in the task definition will be resolved and passed into the callback. */ - constructor(callback: () => Thenable); + constructor(callback: (resolvedDefinition: TaskDefinition) => Thenable); } /** @@ -6100,6 +6208,11 @@ declare module 'vscode' { */ name: string; + /** + * A detail to show for the task on a second line in places where the task's name is displayed. + */ + detail?: string; + /** * The task's execution engine */ @@ -8184,6 +8297,13 @@ declare module 'vscode' { readonly supportsMultipleEditorsPerDocument?: boolean; }): Disposable; + /** + * Register provider that enables the detection and handling of links within the terminal. + * @param provider The provider that provides the terminal links. + * @return Disposable that unregisters the provider. + */ + export function registerTerminalLinkProvider(provider: TerminalLinkProvider): Disposable; + /** * The currently active color theme as configured in the settings. The active * theme can be changed via the `workbench.colorTheme` setting. @@ -10587,6 +10707,27 @@ declare module 'vscode' { export function createSourceControl(id: string, label: string, rootUri?: Uri): SourceControl; } + /** + * A DebugProtocolMessage is an opaque stand-in type for the [ProtocolMessage](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage) type defined in the Debug Adapter Protocol. + */ + export interface DebugProtocolMessage { + // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage). + } + + /** + * A DebugProtocolSource is an opaque stand-in type for the [Source](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source) type defined in the Debug Adapter Protocol. + */ + export interface DebugProtocolSource { + // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source). + } + + /** + * A DebugProtocolBreakpoint is an opaque stand-in type for the [Breakpoint](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Breakpoint) type defined in the Debug Adapter Protocol. + */ + export interface DebugProtocolBreakpoint { + // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Breakpoint). + } + /** * Configuration for a debug session. */ @@ -10650,6 +10791,15 @@ declare module 'vscode' { * Send a custom request to the debug adapter. */ customRequest(command: string, args?: any): Thenable; + + /** + * Maps a VS Code breakpoint to the corresponding Debug Adapter Protocol (DAP) breakpoint that is managed by the debug adapter of the debug session. + * If no DAP breakpoint exists (either because the VS Code breakpoint was not yet registered or because the debug adapter is not interested in the breakpoint), the value `undefined` is returned. + * + * @param breakpoint A VS Code [breakpoint](#Breakpoint). + * @return A promise that resolves to the Debug Adapter Protocol breakpoint or `undefined`. + */ + getDebugProtocolBreakpoint(breakpoint: Breakpoint): Thenable; } /** @@ -10790,6 +10940,21 @@ declare module 'vscode' { constructor(port: number, host?: string); } + /** + * Represents a debug adapter running as a Named Pipe (on Windows)/UNIX Domain Socket (on non-Windows) based server. + */ + export class DebugAdapterNamedPipeServer { + /** + * The path to the NamedPipe/UNIX Domain Socket. + */ + readonly path: string; + + /** + * Create a description for a debug adapter running as a socket based server. + */ + constructor(path: string); + } + /** * A debug adapter that implements the Debug Adapter Protocol can be registered with VS Code if it implements the DebugAdapter interface. */ @@ -10810,13 +10975,6 @@ declare module 'vscode' { handleMessage(message: DebugProtocolMessage): void; } - /** - * A DebugProtocolMessage is an opaque stand-in type for the [ProtocolMessage](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage) type defined in the Debug Adapter Protocol. - */ - export interface DebugProtocolMessage { - // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage). - } - /** * A debug adapter descriptor for an inline implementation. */ @@ -10828,7 +10986,7 @@ declare module 'vscode' { constructor(implementation: DebugAdapter); } - export type DebugAdapterDescriptor = DebugAdapterExecutable | DebugAdapterServer | DebugAdapterInlineImplementation; + export type DebugAdapterDescriptor = DebugAdapterExecutable | DebugAdapterServer | DebugAdapterNamedPipeServer | DebugAdapterInlineImplementation; export interface DebugAdapterDescriptorFactory { /** @@ -11023,13 +11181,19 @@ declare module 'vscode' { * Defaults to Separate. */ consoleMode?: DebugConsoleMode; - } - /** - * A DebugProtocolSource is an opaque stand-in type for the [Source](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source) type defined in the Debug Adapter Protocol. - */ - export interface DebugProtocolSource { - // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source). + /** + * Controls whether this session should run without debugging, thus ignoring breakpoints. + * When this property is not specified, the value from the parent session (if there is one) is used. + */ + noDebug?: boolean; + + /** + * Controls if the debug session's parent session is shown in the CALL STACK view even if it has only a single child. + * By default, the debug session will never hide its parent. + * If compact is true, debug sessions with a single child are hidden in the CALL STACK view to make the tree more compact. + */ + compact?: boolean; } /** @@ -11148,6 +11312,12 @@ declare module 'vscode' { */ export function startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration, parentSessionOrOptions?: DebugSession | DebugSessionOptions): Thenable; + /** + * Stop the given debug session or stop all debug sessions if session is omitted. + * @param session The [debug session](#DebugSession) to stop; if omitted all sessions are stopped. + */ + export function stopDebugging(session?: DebugSession): Thenable; + /** * Add breakpoints. * @param breakpoints The breakpoints to add. diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index e971059842b..eeac69bcee3 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -112,6 +112,7 @@ declare module 'vscode' { export function registerAuthenticationProvider(provider: AuthenticationProvider): Disposable; /** + * @deprecated - getSession should now trigger extension activation. * Fires with the provider id that was registered or unregistered. */ export const onDidChangeAuthenticationProviders: Event; @@ -737,19 +738,20 @@ declare module 'vscode' { //#region debug - export interface DebugSessionOptions { - /** - * Controls whether this session should run without debugging, thus ignoring breakpoints. - * When this property is not specified, the value from the parent session (if there is one) is used. - */ - noDebug?: boolean; + /** + * A DebugProtocolVariableContainer is an opaque stand-in type for the intersection of the Scope and Variable types defined in the Debug Adapter Protocol. + * See https://microsoft.github.io/debug-adapter-protocol/specification#Types_Scope and https://microsoft.github.io/debug-adapter-protocol/specification#Types_Variable. + */ + export interface DebugProtocolVariableContainer { + // Properties: the intersection of DAP's Scope and Variable types. + } - /** - * Controls if the debug session's parent session is shown in the CALL STACK view even if it has only a single child. - * By default, the debug session will never hide its parent. - * If compact is true, debug sessions with a single child are hidden in the CALL STACK view to make the tree more compact. - */ - compact?: boolean; + /** + * A DebugProtocolVariable is an opaque stand-in type for the Variable type defined in the Debug Adapter Protocol. + * See https://microsoft.github.io/debug-adapter-protocol/specification#Types_Variable. + */ + export interface DebugProtocolVariable { + // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_Variable). } // deprecated debug API @@ -762,15 +764,6 @@ declare module 'vscode' { debugAdapterExecutable?(folder: WorkspaceFolder | undefined, token?: CancellationToken): ProviderResult; } - export namespace debug { - - /** - * Stop the given debug session or stop all debug sessions if session is omitted. - * @param session The [debug session](#DebugSession) to stop; if omitted all sessions are stopped. - */ - export function stopDebugging(session?: DebugSession): Thenable; - } - //#endregion //#region LogLevel: https://github.com/microsoft/vscode/issues/85992 @@ -927,93 +920,6 @@ declare module 'vscode' { //#endregion - //#region Terminal link handlers https://github.com/microsoft/vscode/issues/91606 - - export namespace window { - /** - * Register a [TerminalLinkHandler](#TerminalLinkHandler) that can be used to intercept and - * handle links that are activated within terminals. - * @param handler The link handler being registered. - * @return A disposable that unregisters the link handler. - */ - export function registerTerminalLinkHandler(handler: TerminalLinkHandler): Disposable; - } - - /** - * Describes how to handle terminal links. - */ - export interface TerminalLinkHandler { - /** - * Handles a link that is activated within the terminal. - * - * @param terminal The terminal the link was activated on. - * @param link The text of the link activated. - * @return Whether the link was handled, if the link was handled this link will not be - * considered by any other extension or by the default built-in link handler. - */ - handleLink(terminal: Terminal, link: string): ProviderResult; - } - - //#endregion - - //#region Terminal link provider https://github.com/microsoft/vscode/issues/91606 - - export namespace window { - export function registerTerminalLinkProvider(provider: TerminalLinkProvider): Disposable; - } - - export interface TerminalLinkContext { - /** - * This is the text from the unwrapped line in the terminal. - */ - line: string; - - /** - * The terminal the link belongs to. - */ - terminal: Terminal; - } - - export interface TerminalLinkProvider { - /** - * Provide terminal links for the given context. Note that this can be called multiple times - * even before previous calls resolve, make sure to not share global objects (eg. `RegExp`) - * that could have problems when asynchronous usage may overlap. - * @param context Information about what links are being provided for. - * @param token A cancellation token. - * @return A list of terminal links for the given line. - */ - provideTerminalLinks(context: TerminalLinkContext, token: CancellationToken): ProviderResult - - /** - * Handle an activated terminal link. - */ - handleTerminalLink(link: T): ProviderResult; - } - - export interface TerminalLink { - /** - * The start index of the link on [TerminalLinkContext.line](#TerminalLinkContext.line]. - */ - startIndex: number; - - /** - * The length of the link on [TerminalLinkContext.line](#TerminalLinkContext.line] - */ - length: number; - - /** - * The tooltip text when you hover over this link. - * - * If a tooltip is provided, is will be displayed in a string that includes instructions on - * how to trigger the link, such as `{0} (ctrl + click)`. The specific instructions vary - * depending on OS, user settings, and localization. - */ - tooltip?: string; - } - - //#endregion - //#region @jrieken -> exclusive document filters export interface DocumentFilter { @@ -1074,27 +980,6 @@ declare module 'vscode' { } //#endregion - //#region CustomExecution: https://github.com/microsoft/vscode/issues/81007 - /** - * A task to execute - */ - export class Task2 extends Task { - detail?: string; - } - - export class CustomExecution2 extends CustomExecution { - /** - * Constructs a CustomExecution task object. The callback will be executed the task is run, at which point the - * extension should return the Pseudoterminal it will "run in". The task should wait to do further execution until - * [Pseudoterminal.open](#Pseudoterminal.open) is called. Task cancellation should be handled using - * [Pseudoterminal.close](#Pseudoterminal.close). When the task is complete fire - * [Pseudoterminal.onDidClose](#Pseudoterminal.onDidClose). - * @param callback The callback that will be called when the task is started by a user. - */ - constructor(callback: (resolvedDefinition?: TaskDefinition) => Thenable); - } - //#endregion - //#region Task presentation group: https://github.com/microsoft/vscode/issues/47265 export interface TaskPresentationOptions { /** @@ -1171,9 +1056,11 @@ declare module 'vscode' { * @param position The position at which the command was invoked. * @param token A cancellation token. * @return A list of ranges that can be live-renamed togehter. The ranges must have - * identical length and contain identical text content. The ranges cannot overlap. + * identical length and contain identical text content. The ranges cannot overlap. Optional a word pattern + * that overrides the word pattern defined when registering the provider. Live rename stops as soon as the renamed content + * no longer matches the word pattern. */ - provideOnTypeRenameRanges(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + provideOnTypeRenameRanges(document: TextDocument, position: Position, token: CancellationToken): ProviderResult<{ ranges: Range[]; wordPattern?: RegExp; }>; } namespace languages { @@ -1186,10 +1073,10 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider An on type rename provider. - * @param stopPattern Stop on type renaming when input text matches the regular expression. Defaults to `^\s`. + * @param wordPattern Word pattern for this provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ - export function registerOnTypeRenameProvider(selector: DocumentSelector, provider: OnTypeRenameProvider, stopPattern?: RegExp): Disposable; + export function registerOnTypeRenameProvider(selector: DocumentSelector, provider: OnTypeRenameProvider, wordPattern?: RegExp): Disposable; } //#endregion @@ -1309,7 +1196,7 @@ declare module 'vscode' { export interface NotebookCellMetadata { /** - * Controls if the content of a cell is editable or not. + * Controls whether a cell's editor is editable/readonly. */ editable?: boolean; @@ -1377,7 +1264,7 @@ declare module 'vscode' { readonly uri: Uri; readonly cellKind: CellKind; readonly document: TextDocument; - language: string; + readonly language: string; outputs: CellOutput[]; metadata: NotebookCellMetadata; } @@ -1428,11 +1315,12 @@ declare module 'vscode' { export interface NotebookDocument { readonly uri: Uri; + readonly version: number; readonly fileName: string; readonly viewType: string; readonly isDirty: boolean; readonly isUntitled: boolean; - readonly cells: NotebookCell[]; + readonly cells: ReadonlyArray; languages: string[]; displayOrder?: GlobPattern[]; metadata: NotebookDocumentMetadata; @@ -1457,11 +1345,45 @@ declare module 'vscode' { contains(uri: Uri): boolean } + export interface WorkspaceEdit { + replaceCells(uri: Uri, start: number, end: number, cells: NotebookCellData[], metadata?: WorkspaceEditEntryMetadata): void; + replaceCellOutput(uri: Uri, index: number, outputs: CellOutput[], metadata?: WorkspaceEditEntryMetadata): void; + replaceCellMetadata(uri: Uri, index: number, cellMetadata: NotebookCellMetadata, metadata?: WorkspaceEditEntryMetadata): void; + } + export interface NotebookEditorCellEdit { + + replaceCells(start: number, end: number, cells: NotebookCellData[]): void; + replaceOutput(index: number, outputs: CellOutput[]): void; + replaceMetadata(index: number, metadata: NotebookCellMetadata): void; + + /** @deprecated */ insert(index: number, content: string | string[], language: string, type: CellKind, outputs: CellOutput[], metadata: NotebookCellMetadata | undefined): void; + /** @deprecated */ delete(index: number): void; } + export interface NotebookCellRange { + readonly start: number; + readonly end: number; + } + + export enum NotebookEditorRevealType { + /** + * The range will be revealed with as little scrolling as possible. + */ + Default = 0, + /** + * The range will always be revealed in the center of the viewport. + */ + InCenter = 1, + /** + * If the range is outside the viewport, it will be revealed in the center of the viewport. + * Otherwise, it will be revealed with as little scrolling as possible. + */ + InCenterIfOutsideViewport = 2, + } + export interface NotebookEditor { /** * The document associated with this notebook editor. @@ -1473,10 +1395,16 @@ declare module 'vscode' { */ readonly selection?: NotebookCell; + + /** + * The current visible ranges in the editor (vertically). + */ + readonly visibleRanges: NotebookCellRange[]; + /** * The column in which this editor shows. */ - viewColumn?: ViewColumn; + readonly viewColumn?: ViewColumn; /** * Whether the panel is active (focused by the user). @@ -1517,6 +1445,8 @@ declare module 'vscode' { asWebviewUri(localResource: Uri): Uri; edit(callback: (editBuilder: NotebookEditorCellEdit) => void): Thenable; + + revealRange(range: NotebookCellRange, revealType?: NotebookEditorRevealType): void; } export interface NotebookOutputSelector { @@ -1529,31 +1459,6 @@ declare module 'vscode' { outputId: string; } - export interface NotebookOutputRenderer { - /** - * - * @returns HTML fragment. We can probably return `CellOutput` instead of string ? - * - */ - render(document: NotebookDocument, request: NotebookRenderRequest): string; - - /** - * Call before HTML from the renderer is executed, and will be called for - * every editor associated with notebook documents where the renderer - * is or was used. - * - * The communication object will only send and receive messages to the - * render API, retrieved via `acquireNotebookRendererApi`, acquired with - * this specific renderer's ID. - * - * If you need to keep an association between the communication object - * and the document for use in the `render()` method, you can use a WeakMap. - */ - resolveNotebook?(document: NotebookDocument, communication: NotebookCommunication): void; - - readonly preloads?: Uri[]; - } - export interface NotebookCellsChangeData { readonly start: number; readonly deletedCount: number; @@ -1604,12 +1509,22 @@ declare module 'vscode' { readonly cell: NotebookCell; } + export interface NotebookEditorSelectionChangeEvent { + readonly notebookEditor: NotebookEditor; + readonly selection?: NotebookCell; + } + + export interface NotebookEditorVisibleRangesChangeEvent { + readonly notebookEditor: NotebookEditor; + readonly visibleRanges: ReadonlyArray; + } + export interface NotebookCellData { readonly cellKind: CellKind; readonly source: string; - language: string; - outputs: CellOutput[]; - metadata: NotebookCellMetadata; + readonly language: string; + readonly outputs: CellOutput[]; + readonly metadata: NotebookCellMetadata | undefined; } export interface NotebookData { @@ -1727,14 +1642,13 @@ declare module 'vscode' { saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Promise; readonly onDidChangeNotebook: Event; backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, cancellation: CancellationToken): Promise; - - kernel?: NotebookKernel; } export interface NotebookKernel { readonly id?: string; label: string; description?: string; + detail?: string; isPreferred?: boolean; preloads?: Uri[]; executeCell(document: NotebookDocument, cell: NotebookCell): void; @@ -1744,21 +1658,61 @@ declare module 'vscode' { } export interface NotebookDocumentFilter { - viewType?: string; - filenamePattern?: GlobPattern; - excludeFileNamePattern?: GlobPattern; + viewType?: string | string[]; + filenamePattern?: GlobPattern | { include: GlobPattern; exclude: GlobPattern }; } export interface NotebookKernelProvider { - onDidChangeKernels?: Event; + onDidChangeKernels?: Event; provideKernels(document: NotebookDocument, token: CancellationToken): ProviderResult; resolveKernel?(kernel: T, document: NotebookDocument, webview: NotebookCommunication, token: CancellationToken): ProviderResult; } + /** + * Represents the alignment of status bar items. + */ + export enum NotebookCellStatusBarAlignment { + + /** + * Aligned to the left side. + */ + Left = 1, + + /** + * Aligned to the right side. + */ + Right = 2 + } + + export interface NotebookCellStatusBarItem { + readonly cell: NotebookCell; + readonly alignment: NotebookCellStatusBarAlignment; + readonly priority?: number; + text: string; + tooltip: string | undefined; + command: string | Command | undefined; + accessibilityInformation?: AccessibilityInformation; + show(): void; + hide(): void; + dispose(): void; + } + export namespace notebook { export function registerNotebookContentProvider( notebookType: string, - provider: NotebookContentProvider + provider: NotebookContentProvider, + options?: { + /** + * Controls if outputs change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit the outputs in the file document, this should be set to true. + */ + transientOutputs: boolean; + /** + * Controls if a meetadata property change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit a metadata property in the file document, it should be set to true. + */ + transientMetadata: { [K in keyof NotebookCellMetadata]?: boolean } + } ): Disposable; export function registerNotebookKernelProvider( @@ -1766,18 +1720,6 @@ declare module 'vscode' { provider: NotebookKernelProvider ): Disposable; - export function registerNotebookKernel( - id: string, - selectors: GlobPattern[], - kernel: NotebookKernel - ): Disposable; - - export function registerNotebookOutputRenderer( - id: string, - outputSelector: NotebookOutputSelector, - renderer: NotebookOutputRenderer - ): Disposable; - export const onDidOpenNotebookDocument: Event; export const onDidCloseNotebookDocument: Event; export const onDidSaveNotebookDocument: Event; @@ -1787,11 +1729,13 @@ declare module 'vscode' { */ export const notebookDocuments: ReadonlyArray; - export let visibleNotebookEditors: NotebookEditor[]; + export const visibleNotebookEditors: NotebookEditor[]; export const onDidChangeVisibleNotebookEditors: Event; - export let activeNotebookEditor: NotebookEditor | undefined; + export const activeNotebookEditor: NotebookEditor | undefined; export const onDidChangeActiveNotebookEditor: Event; + export const onDidChangeNotebookEditorSelection: Event; + export const onDidChangeNotebookEditorVisibleRanges: Event; export const onDidChangeNotebookCells: Event; export const onDidChangeCellOutputs: Event; export const onDidChangeCellLanguage: Event; @@ -1806,6 +1750,17 @@ declare module 'vscode' { export function createConcatTextDocument(notebook: NotebookDocument, selector?: DocumentSelector): NotebookConcatTextDocument; export const onDidChangeActiveNotebookKernel: Event<{ document: NotebookDocument, kernel: NotebookKernel | undefined }>; + + /** + * Creates a notebook cell status bar [item](#NotebookCellStatusBarItem). + * It will be disposed automatically when the notebook document is closed or the cell is deleted. + * + * @param cell The cell on which this item should be shown. + * @param alignment The alignment of the item. + * @param priority The priority of the item. Higher values mean the item should be shown more to the left. + * @return A new status bar item. + */ + export function createCellStatusBarItem(cell: NotebookCell, alignment?: NotebookCellStatusBarAlignment, priority?: number): NotebookCellStatusBarItem; } //#endregion @@ -2046,59 +2001,173 @@ declare module 'vscode' { } //#endregion - //#region https://github.com/microsoft/vscode/issues/101857 + + //#region https://github.com/microsoft/vscode/issues/104436 + + export enum ExtensionRuntime { + /** + * The extension is running in a NodeJS extension host. Runtime access to NodeJS APIs is available. + */ + Node = 1, + /** + * The extension is running in a Webworker extension host. Runtime access is limited to Webworker APIs. + */ + Webworker = 2 + } export interface ExtensionContext { - - /** - * The uri of a directory in which the extension can create log files. - * The directory might not exist on disk and creation is up to the extension. However, - * the parent directory is guaranteed to be existent. - * - * @see [`workspace.fs`](#FileSystem) for how to read and write files and folders from - * an uri. - */ - readonly logUri: Uri; - - /** - * The uri of a workspace specific directory in which the extension - * can store private state. The directory might not exist and creation is - * up to the extension. However, the parent directory is guaranteed to be existent. - * The value is `undefined` when no workspace nor folder has been opened. - * - * Use [`workspaceState`](#ExtensionContext.workspaceState) or - * [`globalState`](#ExtensionContext.globalState) to store key value data. - * - * @see [`workspace.fs`](#FileSystem) for how to read and write files and folders from - * an uri. - */ - readonly storageUri: Uri | undefined; - - /** - * The uri of a directory in which the extension can store global state. - * The directory might not exist on disk and creation is - * up to the extension. However, the parent directory is guaranteed to be existent. - * - * Use [`globalState`](#ExtensionContext.globalState) to store key value data. - * - * @see [`workspace.fs`](#FileSystem) for how to read and write files and folders from - * an uri. - */ - readonly globalStorageUri: Uri; - - /** - * @deprecated Use [logUri](#ExtensionContext.logUri) instead. - */ - readonly logPath: string; - /** - * @deprecated Use [storagePath](#ExtensionContent.storageUri) instead. - */ - readonly storagePath: string | undefined; - /** - * @deprecated Use [globalStoragePath](#ExtensionContent.globalStorageUri) instead. - */ - readonly globalStoragePath: string; + readonly extensionRuntime: ExtensionRuntime; } //#endregion + + + //#region https://github.com/microsoft/vscode/issues/102091 + + export interface TextDocument { + + /** + * The [notebook](#NotebookDocument) that contains this document as a notebook cell or `undefined` when + * the document is not contained by a notebook (this should be the more frequent case). + */ + notebook: NotebookDocument | undefined; + } + //#endregion + + + //#region https://github.com/microsoft/vscode/issues/46585 + + /** + * A webview based view. + */ + export interface WebviewView { + /** + * Identifies the type of the webview view, such as `'hexEditor.dataView'`. + */ + readonly viewType: string; + + /** + * The underlying webview for the view. + */ + readonly webview: Webview; + + /** + * View title displayed in the UI. + * + * The view title is initially taken from the extension `package.json` contribution. + */ + title?: string; + + /** + * Event fired when the view is disposed. + * + * Views are disposed of in a few cases: + * + * - When a view is collapsed and `retainContextWhenHidden` has not been set. + * - When a view is hidden by a user. + * + * Trying to use the view after it has been disposed throws an exception. + */ + readonly onDidDispose: Event; + + /** + * Tracks if the webview is currently visible. + * + * Views are visible when they are on the screen and expanded. + */ + readonly visible: boolean; + + /** + * Event fired when the visibility of the view changes + */ + readonly onDidChangeVisibility: Event; + } + + interface WebviewViewResolveContext { + /** + * Persisted state from the webview content. + * + * To save resources, VS Code normally deallocates webview views that are not visible. For example, if the user + * collapse a view or switching to another top level activity, the underlying webview document is deallocates. + * + * You can prevent this behavior by setting `retainContextWhenHidden` in the `WebviewOptions`. However this + * increases resource usage and should be avoided wherever possible. Instead, you can use persisted state to + * save off a webview's state so that it can be quickly recreated as needed. + * + * To save off a persisted state, inside the webview call `acquireVsCodeApi().setState()` with + * any json serializable object. To restore the state again, call `getState()`. For example: + * + * ```js + * // Within the webview + * const vscode = acquireVsCodeApi(); + * + * // Get existing state + * const oldState = vscode.getState() || { value: 0 }; + * + * // Update state + * setState({ value: oldState.value + 1 }) + * ``` + * + * VS Code ensures that the persisted state is saved correctly when a webview is hidden and across + * editor restarts. + */ + readonly state: T | undefined; + } + + /** + * Provider for creating `WebviewView` elements. + */ + export interface WebviewViewProvider { + /** + * Revolves a webview view. + * + * `resolveWebviewView` is called when a view first becomes visible. This may happen when the view is + * first loaded or when the user hides and then shows a view again. + * + * @param webviewView Webview panel to restore. The serializer should take ownership of this panel. The + * provider must set the webview's `.html` and hook up all webview events it is interested in. + * @param context Additional metadata about the view being resolved. + * @param token Cancellation token indicating that the view being provided is no longer needed. + * + * @return Optional thenable indicating that the view has been fully resolved. + */ + resolveWebviewView(webviewView: WebviewView, context: WebviewViewResolveContext, token: CancellationToken): Thenable | void; + } + + namespace window { + /** + * Register a new provider for webview views. + * + * @param viewId Unique id of the view. This should match the `id` from the + * `views` contribution in the package.json. + * @param provider Provider for the webview views. + * + * @return Disposable that unregisters the provider. + */ + export function registerWebviewViewProvider(viewId: string, provider: WebviewViewProvider, options?: { + /** + * Content settings for the webview created for this view. + */ + readonly webviewOptions?: { + /** + * Controls if the webview panel's content (iframe) is kept around even when the panel + * is no longer visible. + * + * Normally the webview's html context is created when the panel becomes visible + * and destroyed when it is hidden. Extensions that have complex state + * or UI can set the `retainContextWhenHidden` to make VS Code keep the webview + * context around, even when the webview moves to a background tab. When a webview using + * `retainContextWhenHidden` becomes hidden, its scripts and other dynamic content are suspended. + * When the panel becomes visible again, the context is automatically restored + * in the exact same state it was in originally. You cannot send messages to a + * hidden webview, even with `retainContextWhenHidden` enabled. + * + * `retainContextWhenHidden` has a high memory overhead and should only be used if + * your panel's context cannot be quickly saved and restored. + */ + readonly retainContextWhenHidden?: boolean; + }; + }): Disposable; + } + //#endregion } diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 3d77009b908..bfabf000891 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -54,7 +54,7 @@ import './mainThreadTreeViews'; import './mainThreadDownloadService'; import './mainThreadUrls'; import './mainThreadWindow'; -import './mainThreadWebview'; +import './mainThreadWebviewManager'; import './mainThreadWorkspace'; import './mainThreadComments'; import './mainThreadNotebook'; diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 7a9e0fc6a64..639c613ad67 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -17,7 +17,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { fromNow } from 'vs/base/common/date'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { Platform, platform } from 'vs/base/common/platform'; const VSO_ALLOWED_EXTENSIONS = ['github.vscode-pull-request-github', 'github.vscode-pull-request-github-insiders', 'vscode.git', 'ms-vsonline.vsonline', 'vscode.github-browser']; @@ -232,6 +232,12 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(info => { this._proxy.$onDidChangeAuthenticationProviders([], [info]); })); + + this._proxy.$setProviders(this.authenticationService.declaredProviders); + + this._register(this.authenticationService.onDidChangeDeclaredProviders(e => { + this._proxy.$setProviders(e); + })); } $getProviderIds(): Promise { @@ -249,7 +255,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } $ensureProvider(id: string): Promise { - return this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(id)); + return this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(id), ActivationKind.Immediate); } $sendDidChangeSessions(id: string, event: modes.AuthenticationSessionsChangeEvent): void { diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts similarity index 54% rename from src/vs/workbench/api/browser/mainThreadWebview.ts rename to src/vs/workbench/api/browser/mainThreadCustomEditors.ts index 0d21c18efe8..9351d89b4bd 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts @@ -7,172 +7,66 @@ import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async import { CancellationToken } from 'vs/base/common/cancellation'; import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableStore, dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, IReference } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { basename } from 'vs/base/common/path'; -import { isWeb } from 'vs/base/common/platform'; import { isEqual, isEqualOrParent, toLocalResource } from 'vs/base/common/resources'; -import { escape } from 'vs/base/common/strings'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as modes from 'vs/editor/common/modes'; import { localize } from 'vs/nls'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; 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 { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; +import { MainThreadWebviewPanels } from 'vs/workbench/api/browser/mainThreadWebviewPanels'; +import { MainThreadWebviews, reviveWebviewExtension } from 'vs/workbench/api/browser/mainThreadWebviews'; import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; -import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor'; -import { IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; -import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { editorGroupToViewColumn } from 'vs/workbench/api/common/shared/editor'; +import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; import { CustomDocumentBackupData } from 'vs/workbench/contrib/customEditor/browser/customEditorInputFactory'; import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { CustomTextEditorModel } from 'vs/workbench/contrib/customEditor/common/customTextEditorModel'; -import { WebviewExtensionDescription, WebviewIcons } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; -import { ICreateWebViewShowOptions, IWebviewWorkbenchService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; +import { IWebviewWorkbenchService } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { extHostNamedCustomer } from '../common/extHostCustomers'; -/** - * Bi-directional map between webview handles and inputs. - */ -class WebviewInputStore { - private readonly _handlesToInputs = new Map(); - private readonly _inputsToHandles = new Map(); - - public add(handle: string, input: WebviewInput): void { - this._handlesToInputs.set(handle, input); - this._inputsToHandles.set(input, handle); - } - - public getHandleForInput(input: WebviewInput): string | undefined { - return this._inputsToHandles.get(input); - } - - public getInputForHandle(handle: string): WebviewInput | undefined { - return this._handlesToInputs.get(handle); - } - - public delete(handle: string): void { - const input = this.getInputForHandle(handle); - this._handlesToInputs.delete(handle); - if (input) { - this._inputsToHandles.delete(input); - } - } - - public get size(): number { - return this._handlesToInputs.size; - } - - [Symbol.iterator](): Iterator { - return this._handlesToInputs.values(); - } -} - -class WebviewViewTypeTransformer { - public constructor( - public readonly prefix: string, - ) { } - - public fromExternal(viewType: string): string { - return this.prefix + viewType; - } - - public toExternal(viewType: string): string | undefined { - return viewType.startsWith(this.prefix) - ? viewType.substr(this.prefix.length) - : undefined; - } -} - -const enum ModelType { +const enum CustomEditorModelType { Custom, Text, } -const webviewPanelViewType = new WebviewViewTypeTransformer('mainThreadWebview-'); +export class MainThreadCustomEditors extends Disposable implements extHostProtocol.MainThreadCustomEditorsShape { -@extHostNamedCustomer(extHostProtocol.MainContext.MainThreadWebviews) -export class MainThreadWebviews extends Disposable implements extHostProtocol.MainThreadWebviewsShape { + private readonly _proxyCustomEditors: extHostProtocol.ExtHostCustomEditorsShape; - private static readonly standardSupportedLinkSchemes = new Set([ - Schemas.http, - Schemas.https, - Schemas.mailto, - Schemas.vscode, - 'vscode-insider', - ]); - - private readonly _proxy: extHostProtocol.ExtHostWebviewsShape; - private readonly _webviewInputs = new WebviewInputStore(); - private readonly _revivers = new Map(); private readonly _editorProviders = new Map(); - private readonly _webviewFromDiffEditorHandles = new Set(); constructor( context: extHostProtocol.IExtHostContext, + private readonly mainThreadWebview: MainThreadWebviews, + private readonly mainThreadWebviewPanels: MainThreadWebviewPanels, @IExtensionService extensionService: IExtensionService, @IWorkingCopyService workingCopyService: IWorkingCopyService, @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, @ICustomEditorService private readonly _customEditorService: ICustomEditorService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, - @IEditorService private readonly _editorService: IEditorService, - @IOpenerService private readonly _openerService: IOpenerService, - @IProductService private readonly _productService: IProductService, - @ITelemetryService private readonly _telemetryService: ITelemetryService, @IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IBackupFileService private readonly _backupService: IBackupFileService, ) { super(); - this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviews); + this._proxyCustomEditors = context.getProxy(extHostProtocol.ExtHostContext.ExtHostCustomEditors); - this._register(_editorService.onDidActiveEditorChange(() => { - const activeInput = this._editorService.activeEditor; - if (activeInput instanceof DiffEditorInput && activeInput.primary instanceof WebviewInput && activeInput.secondary instanceof WebviewInput) { - this.registerWebviewFromDiffEditorListeners(activeInput); - } - - this.updateWebviewViewStates(activeInput); - })); - - this._register(_editorService.onDidVisibleEditorsChange(() => { - this.updateWebviewViewStates(this._editorService.activeEditor); - })); - - // This reviver's only job is to activate extensions. - // This should trigger the real reviver to be registered from the extension host side. - this._register(_webviewWorkbenchService.registerResolver({ - canResolve: (webview: WebviewInput) => { - if (webview instanceof CustomEditorInput) { - extensionService.activateByEvent(`onCustomEditor:${webview.viewType}`); - return false; - } - - const viewType = webviewPanelViewType.toExternal(webview.viewType); - if (typeof viewType === 'string') { - extensionService.activateByEvent(`onWebviewPanel:${viewType}`); - } - return false; - }, - resolveWebview: () => { throw new Error('not implemented'); } - })); - - workingCopyFileService.registerWorkingCopyProvider((editorResource) => { + this._register(workingCopyFileService.registerWorkingCopyProvider((editorResource) => { const matchedWorkingCopies: IWorkingCopy[] = []; for (const workingCopy of workingCopyService.workingCopies) { @@ -183,8 +77,18 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma } } return matchedWorkingCopies; + })); - }); + // This reviver's only job is to activate custom editor extensions. + this._register(_webviewWorkbenchService.registerResolver({ + canResolve: (webview: WebviewInput) => { + if (webview instanceof CustomEditorInput) { + extensionService.activateByEvent(`onCustomEditor:${webview.viewType}`); + } + return false; + }, + resolveWebview: () => { throw new Error('not implemented'); } + })); } dispose() { @@ -193,140 +97,21 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma for (const disposable of this._editorProviders.values()) { disposable.dispose(); } + this._editorProviders.clear(); } - public $createWebviewPanel( - extensionData: extHostProtocol.WebviewExtensionDescription, - handle: extHostProtocol.WebviewPanelHandle, - viewType: string, - title: string, - showOptions: { viewColumn?: EditorViewColumn, preserveFocus?: boolean; }, - options: WebviewInputOptions - ): void { - const mainThreadShowOptions: ICreateWebViewShowOptions = Object.create(null); - if (showOptions) { - mainThreadShowOptions.preserveFocus = !!showOptions.preserveFocus; - mainThreadShowOptions.group = viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn); - } - - const extension = reviveWebviewExtension(extensionData); - const webview = this._webviewWorkbenchService.createWebview(handle, webviewPanelViewType.fromExternal(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), extension); - this.hookupWebviewEventDelegate(handle, webview); - - this._webviewInputs.add(handle, webview); - - /* __GDPR__ - "webviews:createWebviewPanel" : { - "extensionId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this._telemetryService.publicLog('webviews:createWebviewPanel', { extensionId: extension.id.value }); - } - - public $disposeWebview(handle: extHostProtocol.WebviewPanelHandle): void { - const webview = this.getWebviewInput(handle); - webview.dispose(); - } - - public $setTitle(handle: extHostProtocol.WebviewPanelHandle, value: string): void { - const webview = this.getWebviewInput(handle); - webview.setName(value); - } - - public $setIconPath(handle: extHostProtocol.WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void { - const webview = this.getWebviewInput(handle); - webview.iconPath = reviveWebviewIcon(value); - } - - public $setHtml(handle: extHostProtocol.WebviewPanelHandle, value: string): void { - const webview = this.getWebviewInput(handle); - webview.webview.html = value; - } - - public $setOptions(handle: extHostProtocol.WebviewPanelHandle, options: modes.IWebviewOptions): void { - const webview = this.getWebviewInput(handle); - webview.webview.contentOptions = reviveWebviewOptions(options); - } - - public $reveal(handle: extHostProtocol.WebviewPanelHandle, showOptions: extHostProtocol.WebviewPanelShowOptions): void { - const webview = this.getWebviewInput(handle); - if (webview.isDisposed()) { - return; - } - - const targetGroup = this._editorGroupService.getGroup(viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn)) || this._editorGroupService.getGroup(webview.group || 0); - if (targetGroup) { - this._webviewWorkbenchService.revealWebview(webview, targetGroup, !!showOptions.preserveFocus); - } - } - - public async $postMessage(handle: extHostProtocol.WebviewPanelHandle, message: any): Promise { - const webview = this.getWebviewInput(handle); - webview.webview.postMessage(message); - return true; - } - - public $registerSerializer(viewType: string): void { - if (this._revivers.has(viewType)) { - throw new Error(`Reviver for ${viewType} already registered`); - } - - this._revivers.set(viewType, this._webviewWorkbenchService.registerResolver({ - canResolve: (webviewInput) => { - return webviewInput.viewType === webviewPanelViewType.fromExternal(viewType); - }, - resolveWebview: async (webviewInput): Promise => { - const viewType = webviewPanelViewType.toExternal(webviewInput.viewType); - if (!viewType) { - webviewInput.webview.html = MainThreadWebviews.getWebviewResolvedFailedContent(webviewInput.viewType); - return; - } - - const handle = webviewInput.id; - this._webviewInputs.add(handle, webviewInput); - this.hookupWebviewEventDelegate(handle, webviewInput); - - let state = undefined; - if (webviewInput.webview.state) { - try { - state = JSON.parse(webviewInput.webview.state); - } catch (e) { - console.error('Could not load webview state', e, webviewInput.webview.state); - } - } - - try { - await this._proxy.$deserializeWebviewPanel(handle, viewType, webviewInput.getTitle(), state, editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0), webviewInput.webview.options); - } catch (error) { - onUnexpectedError(error); - webviewInput.webview.html = MainThreadWebviews.getWebviewResolvedFailedContent(viewType); - } - } - })); - } - - public $unregisterSerializer(viewType: string): void { - const reviver = this._revivers.get(viewType); - if (!reviver) { - throw new Error(`No reviver for ${viewType} registered`); - } - - reviver.dispose(); - this._revivers.delete(viewType); - } - public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities): void { - this.registerEditorProvider(ModelType.Text, extensionData, viewType, options, capabilities, true); + this.registerEditorProvider(CustomEditorModelType.Text, reviveWebviewExtension(extensionData), viewType, options, capabilities, true); } public $registerCustomEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean): void { - this.registerEditorProvider(ModelType.Custom, extensionData, viewType, options, {}, supportsMultipleEditorsPerDocument); + this.registerEditorProvider(CustomEditorModelType.Custom, reviveWebviewExtension(extensionData), viewType, options, {}, supportsMultipleEditorsPerDocument); } private registerEditorProvider( - modelType: ModelType, - extensionData: extHostProtocol.WebviewExtensionDescription, + modelType: CustomEditorModelType, + extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities, @@ -336,8 +121,6 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma throw new Error(`Provider for ${viewType} already registered`); } - const extension = reviveWebviewExtension(extensionData); - const disposables = new DisposableStore(); disposables.add(this._customEditorService.registerCustomEditorCapabilities(viewType, { @@ -352,8 +135,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma const handle = webviewInput.id; const resource = webviewInput.resource; - this._webviewInputs.add(handle, webviewInput); - this.hookupWebviewEventDelegate(handle, webviewInput); + this.mainThreadWebviewPanels.addWebviewInput(handle, webviewInput); webviewInput.webview.options = options; webviewInput.webview.extension = extension; @@ -362,7 +144,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma modelRef = await this.getOrCreateCustomEditorModel(modelType, resource, viewType, { backupId: webviewInput.backupId }, cancellation); } catch (error) { onUnexpectedError(error); - webviewInput.webview.html = MainThreadWebviews.getWebviewResolvedFailedContent(viewType); + webviewInput.webview.html = this.mainThreadWebview.getWebviewResolvedFailedContent(viewType); return; } @@ -390,16 +172,16 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma webviewInput.onMove(async (newResource: URI) => { const oldModel = modelRef; modelRef = await this.getOrCreateCustomEditorModel(modelType, newResource, viewType, {}, CancellationToken.None); - this._proxy.$onMoveCustomEditor(handle, newResource, viewType); + this._proxyCustomEditors.$onMoveCustomEditor(handle, newResource, viewType); oldModel.dispose(); }); } try { - await this._proxy.$resolveWebviewEditor(resource, handle, viewType, webviewInput.getTitle(), editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0), webviewInput.webview.options, cancellation); + await this._proxyCustomEditors.$resolveWebviewEditor(resource, handle, viewType, webviewInput.getTitle(), editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0), webviewInput.webview.options, cancellation); } catch (error) { onUnexpectedError(error); - webviewInput.webview.html = MainThreadWebviews.getWebviewResolvedFailedContent(viewType); + webviewInput.webview.html = this.mainThreadWebview.getWebviewResolvedFailedContent(viewType); modelRef.dispose(); return; } @@ -422,7 +204,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma } private async getOrCreateCustomEditorModel( - modelType: ModelType, + modelType: CustomEditorModelType, resource: URI, viewType: string, options: { backupId?: string }, @@ -434,15 +216,15 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma } switch (modelType) { - case ModelType.Text: + case CustomEditorModelType.Text: { const model = CustomTextEditorModel.create(this._instantiationService, viewType, resource); return this._customEditorService.models.add(resource, viewType, model); } - case ModelType.Custom: + case CustomEditorModelType.Custom: { - const model = MainThreadCustomEditorModel.create(this._instantiationService, this._proxy, viewType, resource, options, () => { - return Array.from(this._webviewInputs) + const model = MainThreadCustomEditorModel.create(this._instantiationService, this._proxyCustomEditors, viewType, resource, options, () => { + return Array.from(this.mainThreadWebviewPanels.webviewInputs) .filter(editor => editor instanceof CustomEditorInput && isEqual(editor.resource, resource)) as CustomEditorInput[]; }, cancellation, this._backupService); return this._customEditorService.models.add(resource, viewType, model); @@ -460,112 +242,6 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma model.changeContent(); } - private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewPanelHandle, input: WebviewInput) { - const disposables = new DisposableStore(); - - disposables.add(input.webview.onDidClickLink((uri) => this.onDidClickLink(handle, uri))); - disposables.add(input.webview.onMessage((message: any) => { this._proxy.$onMessage(handle, message); })); - disposables.add(input.webview.onMissingCsp((extension: ExtensionIdentifier) => this._proxy.$onMissingCsp(handle, extension.value))); - - disposables.add(input.webview.onDispose(() => { - disposables.dispose(); - - this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => { - this._webviewInputs.delete(handle); - }); - })); - } - - private registerWebviewFromDiffEditorListeners(diffEditorInput: DiffEditorInput): void { - const primary = diffEditorInput.primary as WebviewInput; - const secondary = diffEditorInput.secondary as WebviewInput; - - if (this._webviewFromDiffEditorHandles.has(primary.id) || this._webviewFromDiffEditorHandles.has(secondary.id)) { - return; - } - - this._webviewFromDiffEditorHandles.add(primary.id); - this._webviewFromDiffEditorHandles.add(secondary.id); - - const disposables = new DisposableStore(); - disposables.add(primary.webview.onDidFocus(() => this.updateWebviewViewStates(primary))); - disposables.add(secondary.webview.onDidFocus(() => this.updateWebviewViewStates(secondary))); - disposables.add(diffEditorInput.onDispose(() => { - this._webviewFromDiffEditorHandles.delete(primary.id); - this._webviewFromDiffEditorHandles.delete(secondary.id); - dispose(disposables); - })); - } - - private updateWebviewViewStates(activeEditorInput: IEditorInput | undefined) { - if (!this._webviewInputs.size) { - return; - } - - const viewStates: extHostProtocol.WebviewPanelViewStateData = {}; - - const updateViewStatesForInput = (group: IEditorGroup, topLevelInput: IEditorInput, editorInput: IEditorInput) => { - if (!(editorInput instanceof WebviewInput)) { - return; - } - - editorInput.updateGroup(group.id); - - const handle = this._webviewInputs.getHandleForInput(editorInput); - if (handle) { - viewStates[handle] = { - visible: topLevelInput === group.activeEditor, - active: editorInput === activeEditorInput, - position: editorGroupToViewColumn(this._editorGroupService, group.id), - }; - } - }; - - for (const group of this._editorGroupService.groups) { - for (const input of group.editors) { - if (input instanceof DiffEditorInput) { - updateViewStatesForInput(group, input, input.primary); - updateViewStatesForInput(group, input, input.secondary); - } else { - updateViewStatesForInput(group, input, input); - } - } - } - - if (Object.keys(viewStates).length) { - this._proxy.$onDidChangeWebviewPanelViewStates(viewStates); - } - } - - private onDidClickLink(handle: extHostProtocol.WebviewPanelHandle, link: string): void { - const webview = this.getWebviewInput(handle); - if (this.isSupportedLink(webview, URI.parse(link))) { - this._openerService.open(link, { fromUserGesture: true }); - } - } - - private isSupportedLink(webview: WebviewInput, link: URI): boolean { - if (MainThreadWebviews.standardSupportedLinkSchemes.has(link.scheme)) { - return true; - } - if (!isWeb && this._productService.urlProtocol === link.scheme) { - return true; - } - return !!webview.webview.contentOptions.enableCommandUris && link.scheme === Schemas.command; - } - - private getWebviewInput(handle: extHostProtocol.WebviewPanelHandle): WebviewInput { - const webview = this.tryGetWebviewInput(handle); - if (!webview) { - throw new Error(`Unknown webview handle:${handle}`); - } - return webview; - } - - private tryGetWebviewInput(handle: extHostProtocol.WebviewPanelHandle): WebviewInput | undefined { - return this._webviewInputs.getInputForHandle(handle); - } - private async getCustomEditorModel(resourceComponents: UriComponents, viewType: string) { const resource = URI.revive(resourceComponents); const model = await this._customEditorService.models.get(resource, viewType); @@ -574,37 +250,6 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma } return model; } - - private static getWebviewResolvedFailedContent(viewType: string) { - return ` - - - - - - ${localize('errorMessage', "An error occurred while loading view: {0}", escape(viewType))} - `; - } -} - -function reviveWebviewExtension(extensionData: extHostProtocol.WebviewExtensionDescription): WebviewExtensionDescription { - return { id: extensionData.id, location: URI.revive(extensionData.location) }; -} - -function reviveWebviewOptions(options: modes.IWebviewOptions): WebviewInputOptions { - return { - ...options, - allowScripts: options.enableScripts, - localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(r => URI.revive(r)) : undefined, - }; -} - -function reviveWebviewIcon( - value: { light: UriComponents, dark: UriComponents; } | undefined -): WebviewIcons | undefined { - return value - ? { light: URI.revive(value.light), dark: URI.revive(value.dark) } - : undefined; } namespace HotExitState { @@ -644,7 +289,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod public static async create( instantiationService: IInstantiationService, - proxy: extHostProtocol.ExtHostWebviewsShape, + proxy: extHostProtocol.ExtHostCustomEditorsShape, viewType: string, resource: URI, options: { backupId?: string }, @@ -657,7 +302,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod } constructor( - private readonly _proxy: extHostProtocol.ExtHostWebviewsShape, + private readonly _proxy: extHostProtocol.ExtHostCustomEditorsShape, private readonly _viewType: string, private readonly _editorResource: URI, fromBackup: boolean, @@ -712,7 +357,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod } public get capabilities(): WorkingCopyCapabilities { - return 0; + return WorkingCopyCapabilities.None; } public isDirty(): boolean { @@ -890,10 +535,9 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod } const remoteAuthority = this._environmentService.configuration.remoteAuthority; - const localResrouce = toLocalResource(this._editorResource, remoteAuthority); + const localResource = toLocalResource(this._editorResource, remoteAuthority); - - return this._fileDialogService.pickFileToSave(localResrouce, options?.availableFileSystems); + return this._fileDialogService.pickFileToSave(localResource, options?.availableFileSystems); } public async saveCustomEditorAs(resource: URI, targetResource: URI, _options?: ISaveOptions): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 907f2886311..a723b1a38f1 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -264,6 +264,14 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return Promise.reject(new Error('debug session not found')); } + public $getDebugProtocolBreakpoint(sessionId: DebugSessionUUID, breakpoinId: string): Promise { + const session = this.debugService.getModel().getSession(sessionId, true); + if (session) { + return Promise.resolve(session.getDebugProtocolBreakpoint(breakpoinId)); + } + return Promise.reject(new Error('debug session not found')); + } + public $stopDebugging(sessionId: DebugSessionUUID | undefined): Promise { if (sessionId) { const session = this.debugService.getModel().getSession(sessionId, true); diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index ed1f0d0efc4..d8e199ac0ad 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -8,7 +8,7 @@ import { disposed } from 'vs/base/common/errors'; import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { equals as objectEquals } from 'vs/base/common/objects'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditService, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IRange } from 'vs/editor/common/core/range'; import { ISelection } from 'vs/editor/common/core/selection'; @@ -20,7 +20,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { IOpenerService } from 'vs/platform/opener/common/opener'; import { MainThreadDocumentsAndEditors } from 'vs/workbench/api/browser/mainThreadDocumentsAndEditors'; import { MainThreadTextEditor } from 'vs/workbench/api/browser/mainThreadEditor'; -import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, IExtHostContext, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, IUndoStopOptions, MainThreadTextEditorsShape, TextEditorRevealType, IWorkspaceEditDto, reviveWorkspaceEditDto } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, IExtHostContext, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, IUndoStopOptions, MainThreadTextEditorsShape, TextEditorRevealType, IWorkspaceEditDto, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol'; import { EditorViewColumn, editorGroupToViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -29,6 +29,26 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { openEditorWith } from 'vs/workbench/services/editor/common/editorOpenWith'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { revive } from 'vs/base/common/marshalling'; +import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; + +function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] { + if (!data?.edits) { + return []; + } + + const result: ResourceEdit[] = []; + for (let edit of revive(data).edits) { + if (edit._type === WorkspaceEditType.File) { + result.push(new ResourceFileEdit(edit.oldUri, edit.newUri, edit.options, edit.metadata)); + } else if (edit._type === WorkspaceEditType.Text) { + result.push(new ResourceTextEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata)); + } else if (edit._type === WorkspaceEditType.Cell) { + result.push(new ResourceNotebookCellEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata)); + } + } + return result; +} export class MainThreadTextEditors implements MainThreadTextEditorsShape { @@ -222,8 +242,8 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { } $tryApplyWorkspaceEdit(dto: IWorkspaceEditDto): Promise { - const { edits } = reviveWorkspaceEditDto(dto)!; - return this._bulkEditService.apply({ edits }).then(() => true, _err => false); + const edits = reviveWorkspaceEditDto2(dto); + return this._bulkEditService.apply(edits).then(() => true, _err => false); } $tryInsertSnippet(id: string, template: string, ranges: readonly IRange[], opts: IUndoStopOptions): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index f7195eaa219..b3133ba44c9 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -263,12 +263,19 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha // --- on type rename - $registerOnTypeRenameProvider(handle: number, selector: IDocumentFilterDto[], stopPattern?: IRegExpDto): void { - const revivedStopPattern = stopPattern ? MainThreadLanguageFeatures._reviveRegExp(stopPattern) : undefined; + $registerOnTypeRenameProvider(handle: number, selector: IDocumentFilterDto[], wordPattern?: IRegExpDto): void { + const revivedWordPattern = wordPattern ? MainThreadLanguageFeatures._reviveRegExp(wordPattern) : undefined; this._registrations.set(handle, modes.OnTypeRenameProviderRegistry.register(selector, { - stopPattern: revivedStopPattern, - provideOnTypeRenameRanges: (model: ITextModel, position: EditorPosition, token: CancellationToken): Promise => { - return this._proxy.$provideOnTypeRenameRanges(handle, model.uri, position, token); + wordPattern: revivedWordPattern, + provideOnTypeRenameRanges: async (model: ITextModel, position: EditorPosition, token: CancellationToken): Promise<{ ranges: IRange[]; wordPattern?: RegExp; } | undefined> => { + const res = await this._proxy.$provideOnTypeRenameRanges(handle, model.uri, position, token); + if (res) { + return { + ranges: res.ranges, + wordPattern: res.wordPattern ? MainThreadLanguageFeatures._reviveRegExp(res.wordPattern) : undefined + }; + } + return undefined; } })); } diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 9f627eb2cf4..eb223b41070 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -4,60 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext, INotebookDocumentsAndEditorsDelta } from '../common/extHost.protocol'; -import { Disposable, IDisposable, combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { URI, UriComponents } from 'vs/base/common/uri'; -import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType, CellKind, INotebookKernelInfo, INotebookKernelInfoDto, IEditor, INotebookRendererInfo, IOutputRenderRequest, IOutputRenderResponse, INotebookDocumentFilter } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { IRelativePattern } from 'vs/base/common/glob'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { Emitter } from 'vs/base/common/event'; - -export class MainThreadNotebookDocument extends Disposable { - private _textModel: NotebookTextModel; - - get textModel() { - return this._textModel; - } - - constructor( - private readonly _proxy: ExtHostNotebookShape, - public handle: number, - public viewType: string, - public supportBackup: boolean, - public uri: URI, - @INotebookService readonly notebookService: INotebookService, - @IUndoRedoService readonly undoRedoService: IUndoRedoService, - @ITextModelService modelService: ITextModelService - - ) { - super(); - - this._textModel = new NotebookTextModel(handle, viewType, supportBackup, uri, undoRedoService, modelService); - this._register(this._textModel.onDidModelChangeProxy(e => { - this._proxy.$acceptModelChanged(this.uri, e); - this._proxy.$acceptEditorPropertiesChanged(uri, { selections: { selections: this._textModel.selections }, metadata: null }); - })); - this._register(this._textModel.onDidSelectionChange(e => { - const selectionsChange = e ? { selections: e } : null; - this._proxy.$acceptEditorPropertiesChanged(uri, { selections: selectionsChange, metadata: null }); - })); - } - - dispose() { - // this._textModel.dispose(); - super.dispose(); - } -} +import { combinedDisposable, Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILogService } from 'vs/platform/log/common/log'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; +import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType, CellKind, DisplayOrderKey, ICellEditOperation, ICellRange, IEditor, INotebookDocumentFilter, NotebookCellMetadata, NotebookCellOutputsSplice, NotebookDocumentMetadata, NOTEBOOK_DISPLAY_ORDER, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IMainNotebookController, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ExtHostContext, ExtHostNotebookShape, IExtHostContext, INotebookCellStatusBarEntryDto, INotebookDocumentsAndEditorsDelta, MainContext, MainThreadNotebookShape, NotebookEditorRevealType, NotebookExtensionDescription } from '../common/extHost.protocol'; class DocumentAndEditorState { static ofSets(before: Set, after: Set): { removed: T[], added: T[] } { @@ -97,7 +58,7 @@ class DocumentAndEditorState { const apiEditors = []; for (let id in after.textEditors) { const editor = after.textEditors.get(id)!; - apiEditors.push({ id, documentUri: editor.uri!, selections: editor!.textModel!.selections }); + apiEditors.push({ id, documentUri: editor.uri!, selections: editor!.textModel!.selections, visibleRanges: editor.visibleRanges }); } return { @@ -111,7 +72,8 @@ class DocumentAndEditorState { const addedAPIEditors = editorDelta.added.map(add => ({ id: add.getId(), documentUri: add.uri!, - selections: add.textModel!.selections || [] + selections: add.textModel!.selections || [], + visibleRanges: add.visibleRanges })); const removedAPIEditors = editorDelta.removed.map(removed => removed.getId()); @@ -168,32 +130,33 @@ class DocumentAndEditorState { @extHostNamedCustomer(MainContext.MainThreadNotebook) export class MainThreadNotebooks extends Disposable implements MainThreadNotebookShape { private readonly _notebookProviders = new Map(); - private readonly _notebookKernels = new Map(); - private readonly _notebookKernelProviders = new Map, provider: IDisposable }>(); - private readonly _notebookRenderers = new Map(); + private readonly _notebookKernelProviders = new Map, provider: IDisposable }>(); private readonly _proxy: ExtHostNotebookShape; private _toDisposeOnEditorRemove = new Map(); private _currentState?: DocumentAndEditorState; private _editorEventListenersMapping: Map = new Map(); + private _documentEventListenersMapping: Map = new Map(); + private readonly _cellStatusBarEntries: Map = new Map(); constructor( extHostContext: IExtHostContext, @INotebookService private _notebookService: INotebookService, @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorService private readonly editorService: IEditorService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService - + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, + @ILogService private readonly logService: ILogService, + @INotebookCellStatusBarService private readonly cellStatusBarService: INotebookCellStatusBarService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebook); this.registerListeners(); } - async $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[], renderers: number[]): Promise { + async $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[]): Promise { const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); if (textModel) { - await this._notebookService.transformEditsOutputs(textModel, edits); - return textModel.$applyEdit(modelVersionId, edits, true); + this._notebookService.transformEditsOutputs(textModel, edits); + return textModel.applyEdit(modelVersionId, edits, true); } return false; @@ -201,10 +164,10 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo async removeNotebookTextModel(uri: URI): Promise { // TODO@rebornix, remove cell should use emitDelta as well to ensure document/editor events are sent together - await this._proxy.$acceptDocumentAndEditorsDelta({ removedDocuments: [uri] }); - let textModelDisposableStore = this._editorEventListenersMapping.get(uri.toString()); + this._proxy.$acceptDocumentAndEditorsDelta({ removedDocuments: [uri] }); + let textModelDisposableStore = this._documentEventListenersMapping.get(uri.toString()); textModelDisposableStore?.dispose(); - this._editorEventListenersMapping.delete(URI.from(uri).toString()); + this._documentEventListenersMapping.delete(URI.from(uri).toString()); } private _isDeltaEmpty(delta: INotebookDocumentsAndEditorsDelta) { @@ -264,38 +227,67 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } })); + const notebookEditorAddedHandler = (editor: IEditor) => { + if (!this._editorEventListenersMapping.has(editor.getId())) { + const disposableStore = new DisposableStore(); + disposableStore.add(editor.onDidChangeVisibleRanges(() => { + this._proxy.$acceptEditorPropertiesChanged(editor.getId(), { visibleRanges: { ranges: editor.visibleRanges } }); + })); + + this._editorEventListenersMapping.set(editor.getId(), disposableStore); + } + }; + this._register(this._notebookService.onNotebookEditorAdd(editor => { + notebookEditorAddedHandler(editor); this._addNotebookEditor(editor); })); this._register(this._notebookService.onNotebookEditorsRemove(editors => { this._removeNotebookEditor(editors); + + editors.forEach(editor => { + this._editorEventListenersMapping.get(editor.getId())?.dispose(); + this._editorEventListenersMapping.delete(editor.getId()); + }); })); + this._notebookService.listNotebookEditors().forEach(editor => { + notebookEditorAddedHandler(editor); + }); + + const notebookDocumentAddedHandler = (doc: URI) => { + if (!this._editorEventListenersMapping.has(doc.toString())) { + const disposableStore = new DisposableStore(); + const textModel = this._notebookService.getNotebookTextModel(doc); + disposableStore.add(textModel!.onDidModelChangeProxy(e => { + this._proxy.$acceptModelChanged(textModel!.uri, e, textModel!.isDirty); + this._proxy.$acceptDocumentPropertiesChanged(doc, { selections: { selections: textModel!.selections }, metadata: null }); + })); + disposableStore.add(textModel!.onDidSelectionChange(e => { + const selectionsChange = e ? { selections: e } : null; + this._proxy.$acceptDocumentPropertiesChanged(doc, { selections: selectionsChange, metadata: null }); + })); + + this._editorEventListenersMapping.set(textModel!.uri.toString(), disposableStore); + } + }; + this._register(this._notebookService.onNotebookDocumentAdd((documents) => { documents.forEach(doc => { - if (!this._editorEventListenersMapping.has(doc.toString())) { - const disposableStore = new DisposableStore(); - const textModel = this._notebookService.getNotebookTextModel(doc); - disposableStore.add(textModel!.onDidModelChangeProxy(e => { - this._proxy.$acceptModelChanged(textModel!.uri, e); - this._proxy.$acceptEditorPropertiesChanged(doc, { selections: { selections: textModel!.selections }, metadata: null }); - })); - disposableStore.add(textModel!.onDidSelectionChange(e => { - const selectionsChange = e ? { selections: e } : null; - this._proxy.$acceptEditorPropertiesChanged(doc, { selections: selectionsChange, metadata: null }); - })); - - this._editorEventListenersMapping.set(textModel!.uri.toString(), disposableStore); - } + notebookDocumentAddedHandler(doc); }); this._updateState(); })); + this._notebookService.listNotebookDocuments().forEach((doc) => { + notebookDocumentAddedHandler(doc.uri); + }); + this._register(this._notebookService.onNotebookDocumentRemove((documents) => { documents.forEach(doc => { - this._editorEventListenersMapping.get(doc.toString())?.dispose(); - this._editorEventListenersMapping.delete(doc.toString()); + this._documentEventListenersMapping.get(doc.toString())?.dispose(); + this._documentEventListenersMapping.delete(doc.toString()); }); this._updateState(); @@ -310,7 +302,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo })); const updateOrder = () => { - let userOrder = this.configurationService.getValue('notebook.displayOrder'); + let userOrder = this.configurationService.getValue(DisplayOrderKey); this._proxy.$acceptDisplayOrder({ defaultOrder: this.accessibilityService.isScreenReaderOptimized() ? ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER : NOTEBOOK_DISPLAY_ORDER, userOrder: userOrder @@ -320,7 +312,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo updateOrder(); this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectedKeys.indexOf('notebook.displayOrder') >= 0) { + if (e.affectedKeys.indexOf(DisplayOrderKey) >= 0) { updateOrder(); } })); @@ -412,20 +404,10 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo // } } - async $registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: UriComponents[]): Promise { - const renderer = new MainThreadNotebookRenderer(this._proxy, type, extension.id, URI.revive(extension.location), selectors, preloads.map(uri => URI.revive(uri))); - this._notebookRenderers.set(type, renderer); - this._notebookService.registerNotebookRenderer(type, renderer); - } - - async $unregisterNotebookRenderer(id: string): Promise { - this._notebookService.unregisterNotebookRenderer(id); - } - - async $registerNotebookProvider(_extension: NotebookExtensionDescription, _viewType: string, _supportBackup: boolean, _kernel: INotebookKernelInfoDto | undefined): Promise { + async $registerNotebookProvider(_extension: NotebookExtensionDescription, _viewType: string, _supportBackup: boolean, options: { transientOutputs: boolean; transientMetadata: TransientMetadata }): Promise { const controller: IMainNotebookController = { - kernel: _kernel, supportBackup: _supportBackup, + options: options, reloadNotebook: async (mainthreadTextModel: NotebookTextModel) => { const data = await this._proxy.$resolveNotebookData(_viewType, mainthreadTextModel.uri); if (!data) { @@ -434,16 +416,16 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo mainthreadTextModel.languages = data.languages; mainthreadTextModel.metadata = data.metadata; + mainthreadTextModel.transientOptions = options; const edits: ICellEditOperation[] = [ - { editType: CellEditType.Delete, count: mainthreadTextModel.cells.length, index: 0 }, - { editType: CellEditType.Insert, index: 0, cells: data.cells } + { editType: CellEditType.Replace, index: 0, count: mainthreadTextModel.cells.length, cells: data.cells } ]; - await this._notebookService.transformEditsOutputs(mainthreadTextModel, edits); + this._notebookService.transformEditsOutputs(mainthreadTextModel, edits); await new Promise(resolve => { DOM.scheduleAtNextAnimationFrame(() => { - const ret = mainthreadTextModel!.$applyEdit(mainthreadTextModel!.versionId, edits, true); + const ret = mainthreadTextModel!.applyEdit(mainthreadTextModel!.versionId, edits, true); resolve(ret); }); }); @@ -457,38 +439,27 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo textModel.languages = data.languages; textModel.metadata = data.metadata; + textModel.transientOptions = options; if (data.cells.length) { textModel.initialize(data!.cells); } else { - const mainCell = textModel.createCellTextModel([''], textModel.languages.length ? textModel.languages[0] : '', CellKind.Code, [], undefined); + const mainCell = textModel.createCellTextModel('', textModel.languages.length ? textModel.languages[0] : '', CellKind.Code, [], undefined); textModel.insertTemplateCell(mainCell); } - this._proxy.$acceptEditorPropertiesChanged(textModel.uri, { selections: null, metadata: textModel.metadata }); + this._proxy.$acceptDocumentPropertiesChanged(textModel.uri, { selections: null, metadata: textModel.metadata }); return; }, resolveNotebookEditor: async (viewType: string, uri: URI, editorId: string) => { await this._proxy.$resolveNotebookEditor(viewType, uri, editorId); }, - executeNotebookByAttachedKernel: async (viewType: string, uri: URI) => { - return this.executeNotebookByAttachedKernel(viewType, uri); - }, - cancelNotebookByAttachedKernel: async (viewType: string, uri: URI) => { - return this.cancelNotebookByAttachedKernel(viewType, uri); - }, onDidReceiveMessage: (editorId: string, rendererType: string | undefined, message: unknown) => { this._proxy.$onDidReceiveMessage(editorId, rendererType, message); }, removeNotebookDocument: async (uri: URI) => { return this.removeNotebookTextModel(uri); }, - executeNotebookCell: async (uri: URI, handle: number) => { - return this._proxy.$executeNotebookByAttachedKernel(_viewType, uri, handle); - }, - cancelNotebookCell: async (uri: URI, handle: number) => { - return this._proxy.$cancelNotebookByAttachedKernel(_viewType, uri, handle); - }, save: async (uri: URI, token: CancellationToken) => { return this._proxy.$saveNotebook(_viewType, uri, token); }, @@ -516,21 +487,8 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo return; } - async $registerNotebookKernel(extension: NotebookExtensionDescription, id: string, label: string, selectors: (string | IRelativePattern)[], preloads: UriComponents[]): Promise { - const kernel = new MainThreadNotebookKernel(this._proxy, id, label, selectors, extension.id, URI.revive(extension.location), preloads.map(preload => URI.revive(preload))); - this._notebookKernels.set(id, kernel); - this._notebookService.registerNotebookKernel(kernel); - return; - } - - async $unregisterNotebookKernel(id: string): Promise { - this._notebookKernels.delete(id); - this._notebookService.unregisterNotebookKernel(id); - return; - } - async $registerNotebookKernelProvider(extension: NotebookExtensionDescription, handle: number, documentFilter: INotebookDocumentFilter): Promise { - const emitter = new Emitter(); + const emitter = new Emitter(); const that = this; const provider = this._notebookService.registerNotebookKernelProvider({ providerExtensionId: extension.id.value, @@ -550,9 +508,11 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo return that._proxy.$resolveNotebookKernel(handle, editorId, uri, kernelId, token); }, executeNotebook: (uri: URI, kernelId: string, cellHandle: number | undefined) => { + this.logService.debug('MainthreadNotebooks.registerNotebookKernelProvider#executeNotebook', uri.path, kernelId, cellHandle); return that._proxy.$executeNotebookKernelFromProvider(handle, uri, kernelId, cellHandle); }, cancelNotebook: (uri: URI, kernelId: string, cellHandle: number | undefined) => { + this.logService.debug('MainthreadNotebooks.registerNotebookKernelProvider#cancelNotebook', uri.path, kernelId, cellHandle); return that._proxy.$cancelNotebookKernelFromProvider(handle, uri, kernelId, cellHandle); }, }); @@ -575,44 +535,40 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } } - $onNotebookKernelChange(handle: number): void { + $onNotebookKernelChange(handle: number, uriComponents: UriComponents): void { const entry = this._notebookKernelProviders.get(handle); - entry?.emitter.fire(); + entry?.emitter.fire(uriComponents ? URI.revive(uriComponents) : undefined); } async $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise { + this.logService.debug('MainThreadNotebooks#updateNotebookLanguages', resource.path, languages); const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); textModel?.updateLanguages(languages); } async $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata): Promise { + this.logService.debug('MainThreadNotebooks#updateNotebookMetadata', resource.path, metadata); const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); textModel?.updateNotebookMetadata(metadata); } async $updateNotebookCellMetadata(viewType: string, resource: UriComponents, handle: number, metadata: NotebookCellMetadata): Promise { + this.logService.debug('MainThreadNotebooks#updateNotebookCellMetadata', resource.path, handle, metadata); const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); - textModel?.updateNotebookCellMetadata(handle, metadata); + textModel?.changeCellMetadata(handle, metadata, true); } - async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise { + async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[]): Promise { + this.logService.debug('MainThreadNotebooks#spliceNotebookCellOutputs', resource.path, cellHandle); const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); if (textModel) { - await this._notebookService.transformSpliceOutputs(textModel, splices); - textModel.$spliceNotebookCellOutputs(cellHandle, splices); + this._notebookService.transformSpliceOutputs(textModel, splices); + textModel.spliceNotebookCellOutputs(cellHandle, splices); } } - async executeNotebookByAttachedKernel(viewType: string, uri: URI): Promise { - return this._proxy.$executeNotebookByAttachedKernel(viewType, uri, undefined); - } - - async cancelNotebookByAttachedKernel(viewType: string, uri: URI): Promise { - return this._proxy.$cancelNotebookByAttachedKernel(viewType, uri, undefined); - } - async $postMessage(editorId: string, forRendererId: string | undefined, value: any): Promise { const editor = this._notebookService.getNotebookEditor(editorId) as INotebookEditor | undefined; if (editor?.isNotebookEditor) { @@ -627,7 +583,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); if (textModel) { - textModel.$handleEdit(label, () => { + textModel.handleEdit(label, () => { return this._proxy.$undoNotebook(textModel.viewType, textModel.uri, editId, textModel.isDirty); }, () => { return this._proxy.$redoNotebook(textModel.viewType, textModel.uri, editId, textModel.isDirty); @@ -639,42 +595,49 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); textModel?.handleUnknownChange(); } -} -export class MainThreadNotebookKernel implements INotebookKernelInfo { - constructor( - private readonly _proxy: ExtHostNotebookShape, - readonly id: string, - readonly label: string, - readonly selectors: (string | IRelativePattern)[], - readonly extension: ExtensionIdentifier, - readonly extensionLocation: URI, - readonly preloads: URI[] - ) { + async $tryRevealRange(id: string, range: ICellRange, revealType: NotebookEditorRevealType) { + const editor = this._notebookService.listNotebookEditors().find(editor => editor.getId() === id); + if (editor && editor.isNotebookEditor) { + const notebookEditor = editor as INotebookEditor; + const viewModel = notebookEditor.viewModel; + const cell = viewModel?.viewCells[range.start]; + if (!cell) { + return; + } + + switch (revealType) { + case NotebookEditorRevealType.Default: + notebookEditor.revealInView(cell); + break; + case NotebookEditorRevealType.InCenter: + notebookEditor.revealInCenter(cell); + break; + case NotebookEditorRevealType.InCenterIfOutsideViewport: + notebookEditor.revealInCenterIfOutsideViewport(cell); + break; + default: + break; + } + } } - async executeNotebook(viewType: string, uri: URI, handle: number | undefined): Promise { - return this._proxy.$executeNotebook2(this.id, viewType, uri, handle); + async $setStatusBarEntry(id: number, rawStatusBarEntry: INotebookCellStatusBarEntryDto): Promise { + const statusBarEntry = { + ...rawStatusBarEntry, + ...{ cellResource: URI.revive(rawStatusBarEntry.cellResource) } + }; + + const existingEntry = this._cellStatusBarEntries.get(id); + if (existingEntry) { + existingEntry.dispose(); + } + + if (statusBarEntry.visible) { + this._cellStatusBarEntries.set( + id, + this.cellStatusBarService.addEntry(statusBarEntry)); + } } } -export class MainThreadNotebookRenderer implements INotebookRendererInfo { - constructor( - private readonly _proxy: ExtHostNotebookShape, - readonly id: string, - readonly extensionId: ExtensionIdentifier, - readonly extensionLocation: URI, - readonly selectors: INotebookMimeTypeSelector, - readonly preloads: URI[] - ) { - - } - - render(uri: URI, request: IOutputRenderRequest): Promise | undefined> { - return this._proxy.$renderOutputs(uri, this.id, request); - } - - render2(uri: URI, request: IOutputRenderRequest): Promise | undefined> { - return this._proxy.$renderOutputs2(uri, this.id, request); - } -} diff --git a/src/vs/workbench/api/browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts index 536fd02dbd3..350ce8e8753 100644 --- a/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/src/vs/workbench/api/browser/mainThreadTask.ts @@ -5,14 +5,14 @@ import * as nls from 'vs/nls'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; 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, forEach } from 'vs/base/common/collections'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ContributedTask, ConfiguringTask, KeyedTaskIdentifier, TaskExecution, Task, TaskEvent, TaskEventKind, @@ -414,10 +414,18 @@ export class MainThreadTask implements MainThreadTaskShape { ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTask); this._providers = new Map(); - this._taskService.onDidStateChange((event: TaskEvent) => { + this._taskService.onDidStateChange(async (event: TaskEvent) => { const task = event.__task!; if (event.kind === TaskEventKind.Start) { - this._proxy.$onDidStartTask(TaskExecutionDTO.from(task.getTaskExecution()), event.terminalId!); + const execution = TaskExecutionDTO.from(task.getTaskExecution()); + let resolvedDefinition: TaskDefinitionDTO = execution.task!.definition; + if (execution.task?.execution && CustomExecutionDTO.is(execution.task.execution) && event.resolvedVariables) { + const dictionary: IStringDictionary = {}; + Array.from(event.resolvedVariables.entries()).forEach(entry => dictionary[entry[0]] = entry[1]); + resolvedDefinition = await this._configurationResolverService.resolveAny(task.getWorkspaceFolder(), + execution.task.definition, dictionary); + } + this._proxy.$onDidStartTask(execution, event.terminalId!, resolvedDefinition); } else if (event.kind === TaskEventKind.ProcessStarted) { this._proxy.$onDidStartTaskProcess(TaskProcessStartedDTO.from(task.getTaskExecution(), event.processId!)); } else if (event.kind === TaskEventKind.ProcessEnded) { @@ -509,11 +517,27 @@ export class MainThreadTask implements MainThreadTaskShape { }); } + private getWorkspace(value: UriComponents | string): string | IWorkspace | IWorkspaceFolder | null { + let workspace; + if (typeof value === 'string') { + workspace = value; + } else { + const workspaceObject = this._workspaceContextServer.getWorkspace(); + const uri = URI.revive(value); + if (workspaceObject.configuration?.toString() === uri.toString()) { + workspace = workspaceObject; + } else { + workspace = this._workspaceContextServer.getWorkspaceFolder(uri); + } + } + return workspace; + } + public async $getTaskExecution(value: TaskHandleDTO | TaskDTO): Promise { if (TaskHandleDTO.is(value)) { - const workspaceFolder = typeof value.workspaceFolder === 'string' ? value.workspaceFolder : this._workspaceContextServer.getWorkspaceFolder(URI.revive(value.workspaceFolder)); - if (workspaceFolder) { - const task = await this._taskService.getTask(workspaceFolder, value.id, true); + const workspace = this.getWorkspace(value.workspaceFolder); + if (workspace) { + const task = await this._taskService.getTask(workspace, value.id, true); if (task) { return { id: task._id, @@ -538,9 +562,9 @@ export class MainThreadTask implements MainThreadTaskShape { public $executeTask(value: TaskHandleDTO | TaskDTO): Promise { return new Promise((resolve, reject) => { if (TaskHandleDTO.is(value)) { - const workspaceFolder = typeof value.workspaceFolder === 'string' ? value.workspaceFolder : this._workspaceContextServer.getWorkspaceFolder(URI.revive(value.workspaceFolder)); - if (workspaceFolder) { - this._taskService.getTask(workspaceFolder, value.id, true).then((task: Task | undefined) => { + const workspace = this.getWorkspace(value.workspaceFolder); + if (workspace) { + this._taskService.getTask(workspace, value.id, true).then((task: Task | undefined) => { if (!task) { reject(new Error('Task not found')); } else { diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index ffd7fb0faee..c5c03a86786 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -9,7 +9,7 @@ import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceS import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { URI } from 'vs/base/common/uri'; import { StopWatch } from 'vs/base/common/stopwatch'; -import { ITerminalInstanceService, ITerminalService, ITerminalInstance, ITerminalBeforeHandleLinkEvent, ITerminalExternalLinkProvider, ITerminalLink } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalInstanceService, ITerminalService, ITerminalInstance, ITerminalExternalLinkProvider, ITerminalLink } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; @@ -25,7 +25,6 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape private readonly _toDispose = new DisposableStore(); private readonly _terminalProcessProxies = new Map(); private _dataEventTracker: TerminalDataEventTracker | undefined; - private _linkHandler: IDisposable | undefined; /** * A single shared terminal link provider for the exthost. When an ext registers a link * provider, this is registered with the terminal on the renderer side and all links are @@ -95,7 +94,6 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape public dispose(): void { this._toDispose.dispose(); - this._linkHandler?.dispose(); this._linkProvider?.dispose(); // TODO@Daniel: Should all the previously created terminals be disposed @@ -156,6 +154,10 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._dataEventTracker = this._instantiationService.createInstance(TerminalDataEventTracker, (id, data) => { this._onTerminalData(id, data); }); + // Send initial events if they exist + this._terminalService.terminalInstances.forEach(t => { + t.initialDataEvents?.forEach(d => this._onTerminalData(t.id, d)); + }); } } @@ -166,16 +168,6 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } } - public $startHandlingLinks(): void { - this._linkHandler?.dispose(); - this._linkHandler = this._terminalService.addLinkHandler(this._remoteAuthority || '', e => this._handleLink(e)); - } - - public $stopHandlingLinks(): void { - this._linkHandler?.dispose(); - this._linkHandler = undefined; - } - public $startLinkProvider(): void { this._linkProvider?.dispose(); this._linkProvider = this._terminalService.registerLinkProvider(new ExtensionTerminalLinkProvider(this._proxy)); @@ -186,11 +178,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._linkProvider = undefined; } - private async _handleLink(e: ITerminalBeforeHandleLinkEvent): Promise { - if (!e.terminal) { - return false; - } - return this._proxy.$handleLink(e.terminal.id, e.link); + public $registerProcessSupport(isSupported: boolean): void { + this._terminalService.registerProcessSupport(isSupported); } private _onActiveTerminalChanged(terminalId: number | null): void { diff --git a/src/vs/workbench/api/browser/mainThreadWebviewManager.ts b/src/vs/workbench/api/browser/mainThreadWebviewManager.ts new file mode 100644 index 00000000000..225f15b104c --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadWebviewManager.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 { Disposable } from 'vs/base/common/lifecycle'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { MainThreadCustomEditors } from 'vs/workbench/api/browser/mainThreadCustomEditors'; +import { MainThreadWebviewPanels } from 'vs/workbench/api/browser/mainThreadWebviewPanels'; +import { MainThreadWebviews } from 'vs/workbench/api/browser/mainThreadWebviews'; +import { MainThreadWebviewsViews } from 'vs/workbench/api/browser/mainThreadWebviewViews'; +import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; +import { extHostCustomer } from '../common/extHostCustomers'; + +@extHostCustomer +export class MainThreadWebviewManager extends Disposable { + constructor( + context: extHostProtocol.IExtHostContext, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + const webviews = this._register(instantiationService.createInstance(MainThreadWebviews, context)); + context.set(extHostProtocol.MainContext.MainThreadWebviews, webviews); + + const webviewPanels = this._register(instantiationService.createInstance(MainThreadWebviewPanels, context, webviews)); + context.set(extHostProtocol.MainContext.MainThreadWebviewPanels, webviewPanels); + + const customEditors = this._register(instantiationService.createInstance(MainThreadCustomEditors, context, webviews, webviewPanels)); + context.set(extHostProtocol.MainContext.MainThreadCustomEditors, customEditors); + + const webviewViews = this._register(instantiationService.createInstance(MainThreadWebviewsViews, context, webviews)); + context.set(extHostProtocol.MainContext.MainThreadWebviewViews, webviewViews); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts new file mode 100644 index 00000000000..84107c980a3 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts @@ -0,0 +1,338 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { MainThreadWebviews, reviveWebviewExtension, reviveWebviewOptions } from 'vs/workbench/api/browser/mainThreadWebviews'; +import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; +import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor'; +import { IEditorInput } from 'vs/workbench/common/editor'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { WebviewIcons } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; +import { ICreateWebViewShowOptions, IWebviewWorkbenchService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +/** + * Bi-directional map between webview handles and inputs. + */ +class WebviewInputStore { + private readonly _handlesToInputs = new Map(); + private readonly _inputsToHandles = new Map(); + + public add(handle: string, input: WebviewInput): void { + this._handlesToInputs.set(handle, input); + this._inputsToHandles.set(input, handle); + } + + public getHandleForInput(input: WebviewInput): string | undefined { + return this._inputsToHandles.get(input); + } + + public getInputForHandle(handle: string): WebviewInput | undefined { + return this._handlesToInputs.get(handle); + } + + public delete(handle: string): void { + const input = this.getInputForHandle(handle); + this._handlesToInputs.delete(handle); + if (input) { + this._inputsToHandles.delete(input); + } + } + + public get size(): number { + return this._handlesToInputs.size; + } + + [Symbol.iterator](): Iterator { + return this._handlesToInputs.values(); + } +} + +class WebviewViewTypeTransformer { + public constructor( + public readonly prefix: string, + ) { } + + public fromExternal(viewType: string): string { + return this.prefix + viewType; + } + + public toExternal(viewType: string): string | undefined { + return viewType.startsWith(this.prefix) + ? viewType.substr(this.prefix.length) + : undefined; + } +} + +export class MainThreadWebviewPanels extends Disposable implements extHostProtocol.MainThreadWebviewPanelsShape { + + private readonly webviewPanelViewType = new WebviewViewTypeTransformer('mainThreadWebview-'); + + private readonly _proxy: extHostProtocol.ExtHostWebviewPanelsShape; + + private readonly _webviewInputs = new WebviewInputStore(); + + private readonly _editorProviders = new Map(); + private readonly _webviewFromDiffEditorHandles = new Set(); + + private readonly _revivers = new Map(); + + constructor( + context: extHostProtocol.IExtHostContext, + private readonly _mainThreadWebviews: MainThreadWebviews, + @IExtensionService extensionService: IExtensionService, + @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, + @IEditorService private readonly _editorService: IEditorService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService, + ) { + super(); + + this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviewPanels); + + this._register(_editorService.onDidActiveEditorChange(() => { + const activeInput = this._editorService.activeEditor; + if (activeInput instanceof DiffEditorInput && activeInput.primary instanceof WebviewInput && activeInput.secondary instanceof WebviewInput) { + this.registerWebviewFromDiffEditorListeners(activeInput); + } + + this.updateWebviewViewStates(activeInput); + })); + + this._register(_editorService.onDidVisibleEditorsChange(() => { + this.updateWebviewViewStates(this._editorService.activeEditor); + })); + + // This reviver's only job is to activate extensions. + // This should trigger the real reviver to be registered from the extension host side. + this._register(_webviewWorkbenchService.registerResolver({ + canResolve: (webview: WebviewInput) => { + const viewType = this.webviewPanelViewType.toExternal(webview.viewType); + if (typeof viewType === 'string') { + extensionService.activateByEvent(`onWebviewPanel:${viewType}`); + } + return false; + }, + resolveWebview: () => { throw new Error('not implemented'); } + })); + } + + dispose() { + super.dispose(); + + for (const disposable of this._editorProviders.values()) { + disposable.dispose(); + } + this._editorProviders.clear(); + } + + public get webviewInputs(): Iterable { return this._webviewInputs; } + + public addWebviewInput(handle: extHostProtocol.WebviewHandle, input: WebviewInput): void { + this._webviewInputs.add(handle, input); + this._mainThreadWebviews.addWebview(handle, input.webview); + + input.webview.onDispose(() => { + this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => { + this._webviewInputs.delete(handle); + }); + }); + } + + public $createWebviewPanel( + extensionData: extHostProtocol.WebviewExtensionDescription, + handle: extHostProtocol.WebviewHandle, + viewType: string, + title: string, + showOptions: { viewColumn?: EditorViewColumn, preserveFocus?: boolean; }, + options: WebviewInputOptions + ): void { + const mainThreadShowOptions: ICreateWebViewShowOptions = Object.create(null); + if (showOptions) { + mainThreadShowOptions.preserveFocus = !!showOptions.preserveFocus; + mainThreadShowOptions.group = viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn); + } + + const extension = reviveWebviewExtension(extensionData); + + const webview = this._webviewWorkbenchService.createWebview(handle, this.webviewPanelViewType.fromExternal(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), extension); + this.addWebviewInput(handle, webview); + + /* __GDPR__ + "webviews:createWebviewPanel" : { + "extensionId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this._telemetryService.publicLog('webviews:createWebviewPanel', { extensionId: extension.id.value }); + } + + public $disposeWebview(handle: extHostProtocol.WebviewHandle): void { + const webview = this.getWebviewInput(handle); + webview.dispose(); + } + + public $setTitle(handle: extHostProtocol.WebviewHandle, value: string): void { + const webview = this.getWebviewInput(handle); + webview.setName(value); + } + + + public $setIconPath(handle: extHostProtocol.WebviewHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void { + const webview = this.getWebviewInput(handle); + webview.iconPath = reviveWebviewIcon(value); + } + + public $reveal(handle: extHostProtocol.WebviewHandle, showOptions: extHostProtocol.WebviewPanelShowOptions): void { + const webview = this.getWebviewInput(handle); + if (webview.isDisposed()) { + return; + } + + const targetGroup = this._editorGroupService.getGroup(viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn)) || this._editorGroupService.getGroup(webview.group || 0); + if (targetGroup) { + this._webviewWorkbenchService.revealWebview(webview, targetGroup, !!showOptions.preserveFocus); + } + } + + public $registerSerializer(viewType: string) + : void { + if (this._revivers.has(viewType)) { + throw new Error(`Reviver for ${viewType} already registered`); + } + + this._revivers.set(viewType, this._webviewWorkbenchService.registerResolver({ + canResolve: (webviewInput) => { + return webviewInput.viewType === this.webviewPanelViewType.fromExternal(viewType); + }, + resolveWebview: async (webviewInput): Promise => { + const viewType = this.webviewPanelViewType.toExternal(webviewInput.viewType); + if (!viewType) { + webviewInput.webview.html = this._mainThreadWebviews.getWebviewResolvedFailedContent(webviewInput.viewType); + return; + } + + + const handle = webviewInput.id; + + this.addWebviewInput(handle, webviewInput); + + let state = undefined; + if (webviewInput.webview.state) { + try { + state = JSON.parse(webviewInput.webview.state); + } catch (e) { + console.error('Could not load webview state', e, webviewInput.webview.state); + } + } + + try { + await this._proxy.$deserializeWebviewPanel(handle, viewType, webviewInput.getTitle(), state, editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0), webviewInput.webview.options); + } catch (error) { + onUnexpectedError(error); + webviewInput.webview.html = this._mainThreadWebviews.getWebviewResolvedFailedContent(viewType); + } + } + })); + } + + public $unregisterSerializer(viewType: string): void { + const reviver = this._revivers.get(viewType); + if (!reviver) { + throw new Error(`No reviver for ${viewType} registered`); + } + + reviver.dispose(); + this._revivers.delete(viewType); + } + + private registerWebviewFromDiffEditorListeners(diffEditorInput: DiffEditorInput): void { + const primary = diffEditorInput.primary as WebviewInput; + const secondary = diffEditorInput.secondary as WebviewInput; + + if (this._webviewFromDiffEditorHandles.has(primary.id) || this._webviewFromDiffEditorHandles.has(secondary.id)) { + return; + } + + this._webviewFromDiffEditorHandles.add(primary.id); + this._webviewFromDiffEditorHandles.add(secondary.id); + + const disposables = new DisposableStore(); + disposables.add(primary.webview.onDidFocus(() => this.updateWebviewViewStates(primary))); + disposables.add(secondary.webview.onDidFocus(() => this.updateWebviewViewStates(secondary))); + disposables.add(diffEditorInput.onDispose(() => { + this._webviewFromDiffEditorHandles.delete(primary.id); + this._webviewFromDiffEditorHandles.delete(secondary.id); + dispose(disposables); + })); + } + + private updateWebviewViewStates(activeEditorInput: IEditorInput | undefined) { + if (!this._webviewInputs.size) { + return; + } + + const viewStates: extHostProtocol.WebviewPanelViewStateData = {}; + + const updateViewStatesForInput = (group: IEditorGroup, topLevelInput: IEditorInput, editorInput: IEditorInput) => { + if (!(editorInput instanceof WebviewInput)) { + return; + } + + editorInput.updateGroup(group.id); + + const handle = this._webviewInputs.getHandleForInput(editorInput); + if (handle) { + viewStates[handle] = { + visible: topLevelInput === group.activeEditor, + active: editorInput === activeEditorInput, + position: editorGroupToViewColumn(this._editorGroupService, group.id), + }; + } + }; + + for (const group of this._editorGroupService.groups) { + for (const input of group.editors) { + if (input instanceof DiffEditorInput) { + updateViewStatesForInput(group, input, input.primary); + updateViewStatesForInput(group, input, input.secondary); + } else { + updateViewStatesForInput(group, input, input); + } + } + } + + if (Object.keys(viewStates).length) { + this._proxy.$onDidChangeWebviewPanelViewStates(viewStates); + } + } + + private getWebviewInput(handle: extHostProtocol.WebviewHandle): WebviewInput { + const webview = this.tryGetWebviewInput(handle); + if (!webview) { + throw new Error(`Unknown webview handle:${handle}`); + } + return webview; + } + + private tryGetWebviewInput(handle: extHostProtocol.WebviewHandle): WebviewInput | undefined { + return this._webviewInputs.getInputForHandle(handle); + } +} + + +function reviveWebviewIcon( + value: { light: UriComponents, dark: UriComponents; } | undefined +): WebviewIcons | undefined { + return value + ? { light: URI.revive(value.light), dark: URI.revive(value.dark) } + : undefined; +} + diff --git a/src/vs/workbench/api/browser/mainThreadWebviewViews.ts b/src/vs/workbench/api/browser/mainThreadWebviewViews.ts new file mode 100644 index 00000000000..248af0860e6 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadWebviewViews.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 { onUnexpectedError } from 'vs/base/common/errors'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { MainThreadWebviews } from 'vs/workbench/api/browser/mainThreadWebviews'; +import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; +import { IWebviewViewService, WebviewView } from 'vs/workbench/contrib/webviewView/browser/webviewViewService'; + + +export class MainThreadWebviewsViews extends Disposable implements extHostProtocol.MainThreadWebviewViewsShape { + + private readonly _proxy: extHostProtocol.ExtHostWebviewViewsShape; + + private readonly _webviewViews = new Map(); + private readonly _webviewViewProviders = new Map(); + + constructor( + context: extHostProtocol.IExtHostContext, + private readonly mainThreadWebviews: MainThreadWebviews, + @IWebviewViewService private readonly _webviewViewService: IWebviewViewService, + ) { + super(); + + this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviewViews); + } + + public $setWebviewViewTitle(handle: extHostProtocol.WebviewHandle, value: string | undefined): void { + const webviewView = this._webviewViews.get(handle); + if (!webviewView) { + throw new Error('unknown webview view'); + } + webviewView.title = value; + } + + public $registerWebviewViewProvider(viewType: string, options?: { retainContextWhenHidden?: boolean }): void { + if (this._webviewViewProviders.has(viewType)) { + throw new Error(`View provider for ${viewType} already registered`); + } + + this._webviewViewService.register(viewType, { + resolve: async (webviewView: WebviewView, cancellation: CancellationToken) => { + const handle = webviewView.webview.id; + + this._webviewViews.set(handle, webviewView); + this.mainThreadWebviews.addWebview(handle, webviewView.webview); + + let state = undefined; + if (webviewView.webview.state) { + try { + state = JSON.parse(webviewView.webview.state); + } catch (e) { + console.error('Could not load webview state', e, webviewView.webview.state); + } + } + + if (options) { + webviewView.webview.options = options; + } + + webviewView.onDidChangeVisibility(visible => { + this._proxy.$onDidChangeWebviewViewVisibility(handle, visible); + }); + + webviewView.onDispose(() => { + this._proxy.$disposeWebviewView(handle); + this._webviewViews.delete(handle); + }); + + try { + await this._proxy.$resolveWebviewView(handle, viewType, state, cancellation); + } catch (error) { + onUnexpectedError(error); + webviewView.webview.html = this.mainThreadWebviews.getWebviewResolvedFailedContent(viewType); + } + } + }); + } + + public $unregisterWebviewViewProvider(viewType: string): void { + const provider = this._webviewViewProviders.get(viewType); + if (!provider) { + throw new Error(`No view provider for ${viewType} registered`); + } + + provider.dispose(); + this._webviewViewProviders.delete(viewType); + } +} + diff --git a/src/vs/workbench/api/browser/mainThreadWebviews.ts b/src/vs/workbench/api/browser/mainThreadWebviews.ts new file mode 100644 index 00000000000..8180b9972ee --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadWebviews.ts @@ -0,0 +1,125 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { isWeb } from 'vs/base/common/platform'; +import { escape } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; +import { IWebviewOptions } from 'vs/editor/common/modes'; +import { localize } from 'vs/nls'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IProductService } from 'vs/platform/product/common/productService'; +import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; +import { Webview, WebviewExtensionDescription, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; + +export class MainThreadWebviews extends Disposable implements extHostProtocol.MainThreadWebviewsShape { + + private static readonly standardSupportedLinkSchemes = new Set([ + Schemas.http, + Schemas.https, + Schemas.mailto, + Schemas.vscode, + 'vscode-insider', + ]); + + private readonly _proxy: extHostProtocol.ExtHostWebviewsShape; + + private readonly _webviews = new Map(); + + constructor( + context: extHostProtocol.IExtHostContext, + @IOpenerService private readonly _openerService: IOpenerService, + @IProductService private readonly _productService: IProductService, + ) { + super(); + + this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviews); + } + + public addWebview(handle: extHostProtocol.WebviewHandle, webview: WebviewOverlay): void { + this._webviews.set(handle, webview); + this.hookupWebviewEventDelegate(handle, webview); + } + + public $setHtml(handle: extHostProtocol.WebviewHandle, value: string): void { + const webview = this.getWebview(handle); + webview.html = value; + } + + public $setOptions(handle: extHostProtocol.WebviewHandle, options: IWebviewOptions): void { + const webview = this.getWebview(handle); + webview.contentOptions = reviveWebviewOptions(options); + } + + public async $postMessage(handle: extHostProtocol.WebviewHandle, message: any): Promise { + const webview = this.getWebview(handle); + webview.postMessage(message); + return true; + } + + private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewHandle, webview: WebviewOverlay) { + const disposables = new DisposableStore(); + + disposables.add(webview.onDidClickLink((uri) => this.onDidClickLink(handle, uri))); + disposables.add(webview.onMessage((message: any) => { this._proxy.$onMessage(handle, message); })); + disposables.add(webview.onMissingCsp((extension: ExtensionIdentifier) => this._proxy.$onMissingCsp(handle, extension.value))); + + disposables.add(webview.onDispose(() => { + disposables.dispose(); + this._webviews.delete(handle); + })); + } + + private onDidClickLink(handle: extHostProtocol.WebviewHandle, link: string): void { + const webview = this.getWebview(handle); + if (this.isSupportedLink(webview, URI.parse(link))) { + this._openerService.open(link, { fromUserGesture: true }); + } + } + + private isSupportedLink(webview: Webview, link: URI): boolean { + if (MainThreadWebviews.standardSupportedLinkSchemes.has(link.scheme)) { + return true; + } + if (!isWeb && this._productService.urlProtocol === link.scheme) { + return true; + } + return !!webview.contentOptions.enableCommandUris && link.scheme === Schemas.command; + } + + private getWebview(handle: extHostProtocol.WebviewHandle): Webview { + const webview = this._webviews.get(handle); + if (!webview) { + throw new Error(`Unknown webview handle:${handle}`); + } + return webview; + } + + public getWebviewResolvedFailedContent(viewType: string) { + return ` + + + + + + ${localize('errorMessage', "An error occurred while loading view: {0}", escape(viewType))} + `; + } +} + +export function reviveWebviewExtension(extensionData: extHostProtocol.WebviewExtensionDescription): WebviewExtensionDescription { + return { id: extensionData.id, location: URI.revive(extensionData.location) }; +} + +export function reviveWebviewOptions(options: IWebviewOptions): WebviewInputOptions { + return { + ...options, + allowScripts: options.enableScripts, + localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(r => URI.revive(r)) : undefined, + }; +} diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 25dadd40aab..73ca03673bc 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -32,6 +32,7 @@ import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneCont import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Codicon } from 'vs/base/common/codicons'; import { CustomTreeView } from 'vs/workbench/contrib/views/browser/treeView'; +import { WebviewViewPane } from 'vs/workbench/contrib/webviewView/browser/webviewViewPane'; export interface IUserFriendlyViewsContainerDescriptor { id: string; @@ -76,7 +77,15 @@ export const viewsContainersContribution: IJSONSchema = { } }; +enum ViewType { + Tree = 'tree', + Webview = 'webview' +} + + interface IUserFriendlyViewDescriptor { + type?: ViewType; + id: string; name: string; when?: string; @@ -208,11 +217,18 @@ const viewsContribution: IJSONSchema = { } }; -export interface ICustomViewDescriptor extends ITreeViewDescriptor { +export interface ICustomTreeViewDescriptor extends ITreeViewDescriptor { readonly extensionId: ExtensionIdentifier; readonly originalContainerId: string; } +export interface ICustomWebviewViewDescriptor extends IViewDescriptor { + readonly extensionId: ExtensionIdentifier; + readonly originalContainerId: string; +} + +export type ICustomViewDescriptor = ICustomTreeViewDescriptor | ICustomWebviewViewDescriptor; + type ViewContainerExtensionPointType = { [loc: string]: IUserFriendlyViewsContainerDescriptor[] }; const viewsContainersExtensionPoint: IExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'viewsContainers', @@ -442,16 +458,24 @@ class ViewsExtensionHandler implements IWorkbenchContribution { const icon = item.icon ? resources.joinPath(extension.description.extensionLocation, item.icon) : undefined; const initialVisibility = this.convertInitialVisibility(item.visibility); - const viewDescriptor = { + + const type = this.getViewType(item.type); + if (!type) { + collector.error(localize('unknownViewType', "Unknown view type `{0}`.", item.type)); + return null; + } + + const viewDescriptor = { + type: type, + ctorDescriptor: type === ViewType.Tree ? new SyncDescriptor(TreeViewPane) : new SyncDescriptor(WebviewViewPane), id: item.id, name: item.name, - ctorDescriptor: new SyncDescriptor(TreeViewPane), when: ContextKeyExpr.deserialize(item.when), containerIcon: icon || viewContainer?.icon, containerTitle: item.contextualTitle || viewContainer?.name, canToggleVisibility: true, canMoveView: true, - treeView: this.instantiationService.createInstance(CustomTreeView, item.id, item.name), + treeView: type === ViewType.Tree ? this.instantiationService.createInstance(CustomTreeView, item.id, item.name) : undefined, collapsed: this.showCollapsed(container) || initialVisibility === InitialVisibility.Collapsed, order: order, extensionId: extension.description.identifier, @@ -461,6 +485,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { hideByDefault: initialVisibility === InitialVisibility.Hidden }; + viewIds.add(viewDescriptor.id); return viewDescriptor; })); @@ -473,6 +498,16 @@ class ViewsExtensionHandler implements IWorkbenchContribution { this.viewsRegistry.registerViews2(allViewDescriptors); } + private getViewType(type: string | undefined): ViewType | undefined { + if (type === ViewType.Webview) { + return ViewType.Webview; + } + if (!type || type === ViewType.Tree) { + return ViewType.Tree; + } + return undefined; + } + private getDefaultViewContainer(): ViewContainer { return this.viewContainersRegistry.get(EXPLORER)!; } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 97793666ad8..3595cd3e381 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -76,6 +76,9 @@ import { ExtHostTimeline } from 'vs/workbench/api/common/extHostTimeline'; import { ExtHostNotebookConcatDocument } from 'vs/workbench/api/common/extHostNotebookConcatDocument'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer'; +import { ExtHostWebviewViews } from 'vs/workbench/api/common/extHostWebviewView'; +import { ExtHostCustomEditors } from 'vs/workbench/api/common/extHostCustomEditors'; +import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -125,7 +128,8 @@ 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.MainThreadTextEditors))); - const extHostEditors = rpcProtocol.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(rpcProtocol, extHostDocumentsAndEditors)); + const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, initData.environment, extHostLogService, extensionStoragePaths)); + const extHostEditors = rpcProtocol.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(rpcProtocol, extHostDocumentsAndEditors, extHostNotebook)); const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands, extHostLogService)); const extHostEditorInsets = rpcProtocol.set(ExtHostContext.ExtHostEditorInsets, new ExtHostEditorInsets(rpcProtocol.getProxy(MainContext.MainThreadEditorInsets), extHostEditors, initData.environment)); const extHostDiagnostics = rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(rpcProtocol, extHostLogService)); @@ -137,11 +141,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostComment = rpcProtocol.set(ExtHostContext.ExtHostComments, new ExtHostComments(rpcProtocol, extHostCommands, extHostDocuments)); const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress))); const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHosLabelService, new ExtHostLabelService(rpcProtocol)); - const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, initData.uiKind === UIKind.Web ? new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, initData.environment) : new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, initData.environment, extensionStoragePaths)); const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol)); const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol)); const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands)); - const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, initData.environment, extHostWorkspace, extHostLogService, extHostApiDeprecation, extHostDocuments, extensionStoragePaths)); + const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, initData.environment, extHostWorkspace, extHostLogService, extHostApiDeprecation)); + 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)); // Check that no named customers are missing const expected: ProxyIdentifier[] = values(ExtHostContext); @@ -564,7 +570,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostOutputService.createOutputChannel(name); }, createWebviewPanel(viewType: string, title: string, showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, options?: vscode.WebviewPanelOptions & vscode.WebviewOptions): vscode.WebviewPanel { - return extHostWebviews.createWebviewPanel(extension, viewType, title, showOptions, options); + return extHostWebviewPanels.createWebviewPanel(extension, viewType, title, showOptions, options); }, createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): vscode.WebviewEditorInset { checkProposedApiEnabled(extension); @@ -579,12 +585,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I } return extHostTerminalService.createTerminal(nameOrOptions, shellPath, shellArgs); }, - registerTerminalLinkHandler(handler: vscode.TerminalLinkHandler): vscode.Disposable { - checkProposedApiEnabled(extension); - return extHostTerminalService.registerLinkHandler(handler); - }, registerTerminalLinkProvider(handler: vscode.TerminalLinkProvider): vscode.Disposable { - checkProposedApiEnabled(extension); return extHostTerminalService.registerLinkProvider(handler); }, registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider): vscode.Disposable { @@ -594,10 +595,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostTreeViews.createTreeView(viewId, options, extension); }, registerWebviewPanelSerializer: (viewType: string, serializer: vscode.WebviewPanelSerializer) => { - return extHostWebviews.registerWebviewPanelSerializer(extension, viewType, serializer); + return extHostWebviewPanels.registerWebviewPanelSerializer(extension, viewType, serializer); }, registerCustomEditorProvider: (viewType: string, provider: vscode.CustomTextEditorProvider | vscode.CustomReadonlyEditorProvider, options: { webviewOptions?: vscode.WebviewPanelOptions, supportsMultipleEditorsPerDocument?: boolean } = {}) => { - return extHostWebviews.registerCustomEditorProvider(extension, viewType, provider, options); + return extHostCustomEditors.registerCustomEditorProvider(extension, viewType, provider, options); }, registerDecorationProvider(provider: vscode.DecorationProvider) { checkProposedApiEnabled(extension); @@ -617,6 +618,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, onDidChangeActiveColorTheme(listener, thisArg?, disposables?) { return extHostTheming.onDidChangeActiveColorTheme(listener, thisArg, disposables); + }, + registerWebviewViewProvider(viewId: string, provider: vscode.WebviewViewProvider, options?: { + webviewOptions?: { + retainContextWhenHidden?: boolean + } + }) { + checkProposedApiEnabled(extension); + return extHostWebviewViews.registerWebviewViewProvider(extension, viewId, provider, options?.webviewOptions); } }; @@ -927,7 +936,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, get notebookDocuments(): vscode.NotebookDocument[] { checkProposedApiEnabled(extension); - return extHostNotebook.notebookDocuments; + return extHostNotebook.notebookDocuments.map(d => d.notebookDocument); }, get visibleNotebookEditors() { checkProposedApiEnabled(extension); @@ -941,22 +950,17 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeActiveNotebookKernel; }, - registerNotebookContentProvider: (viewType: string, provider: vscode.NotebookContentProvider) => { + registerNotebookContentProvider: (viewType: string, provider: vscode.NotebookContentProvider, options?: { + transientOutputs: boolean; + transientMetadata: { [K in keyof vscode.NotebookCellMetadata]?: boolean } + }) => { checkProposedApiEnabled(extension); - return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider); - }, - registerNotebookKernel: (id: string, selector: vscode.GlobPattern[], kernel: vscode.NotebookKernel) => { - checkProposedApiEnabled(extension); - return extHostNotebook.registerNotebookKernel(extension, id, selector, kernel); + return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider, options); }, registerNotebookKernelProvider: (selector: vscode.NotebookDocumentFilter, provider: vscode.NotebookKernelProvider) => { checkProposedApiEnabled(extension); return extHostNotebook.registerNotebookKernelProvider(extension, selector, provider); }, - registerNotebookOutputRenderer: (type: string, outputFilter: vscode.NotebookOutputSelector, renderer: vscode.NotebookOutputRenderer) => { - checkProposedApiEnabled(extension); - return extHostNotebook.registerNotebookOutputRenderer(type, extension, outputFilter, renderer); - }, get activeNotebookEditor(): vscode.NotebookEditor | undefined { checkProposedApiEnabled(extension); return extHostNotebook.activeNotebookEditor; @@ -969,6 +973,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeNotebookCells(listener, thisArgs, disposables); }, + onDidChangeNotebookEditorSelection(listener, thisArgs?, disposables?) { + checkProposedApiEnabled(extension); + return extHostNotebook.onDidChangeNotebookEditorSelection(listener, thisArgs, disposables); + }, + onDidChangeNotebookEditorVisibleRanges(listener, thisArgs?, disposables?) { + checkProposedApiEnabled(extension); + return extHostNotebook.onDidChangeNotebookEditorVisibleRanges(listener, thisArgs, disposables); + }, onDidChangeCellOutputs(listener, thisArgs?, disposables?) { checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeCellOutputs(listener, thisArgs, disposables); @@ -984,6 +996,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I createConcatTextDocument(notebook, selector) { checkProposedApiEnabled(extension); return new ExtHostNotebookConcatDocument(extHostNotebook, extHostDocuments, notebook, selector); + }, + createCellStatusBarItem(cell: vscode.NotebookCell, alignment?: vscode.NotebookCellStatusBarAlignment, priority?: number): vscode.NotebookCellStatusBarItem { + checkProposedApiEnabled(extension); + return extHostNotebook.createNotebookCellStatusBarItemInternal(cell, alignment, priority); } }; @@ -1024,6 +1040,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ConfigurationTarget: extHostTypes.ConfigurationTarget, DebugAdapterExecutable: extHostTypes.DebugAdapterExecutable, DebugAdapterServer: extHostTypes.DebugAdapterServer, + DebugAdapterNamedPipeServer: extHostTypes.DebugAdapterNamedPipeServer, DebugAdapterInlineImplementation: extHostTypes.DebugAdapterInlineImplementation, DecorationRangeBehavior: extHostTypes.DecorationRangeBehavior, Diagnostic: extHostTypes.Diagnostic, @@ -1041,8 +1058,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I EventEmitter: Emitter, ExtensionKind: extHostTypes.ExtensionKind, ExtensionMode: extHostTypes.ExtensionMode, + ExtensionRuntime: extHostTypes.ExtensionRuntime, CustomExecution: extHostTypes.CustomExecution, - CustomExecution2: extHostTypes.CustomExecution, FileChangeType: extHostTypes.FileChangeType, FileSystemError: extHostTypes.FileSystemError, FileType: files.FileType, @@ -1085,7 +1102,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I SymbolKind: extHostTypes.SymbolKind, SymbolTag: extHostTypes.SymbolTag, Task: extHostTypes.Task, - Task2: extHostTypes.Task, TaskGroup: extHostTypes.TaskGroup, TaskPanelKind: extHostTypes.TaskPanelKind, TaskRevealKind: extHostTypes.TaskRevealKind, @@ -1117,7 +1133,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I CellKind: extHostTypes.CellKind, CellOutputKind: extHostTypes.CellOutputKind, NotebookCellRunState: extHostTypes.NotebookCellRunState, - NotebookRunState: extHostTypes.NotebookRunState + NotebookRunState: extHostTypes.NotebookRunState, + NotebookCellStatusBarAlignment: extHostTypes.NotebookCellStatusBarAlignment, + NotebookEditorRevealType: extHostTypes.NotebookEditorRevealType }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 6caf716b6c6..4b794666295 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -42,7 +42,7 @@ import { IRevealOptions, ITreeItem } from 'vs/workbench/common/views'; import { IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug'; import { ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder'; import { ITerminalDimensions, IShellLaunchConfig, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions'; +import { ActivationKind, ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions'; import { createExtHostContextProxyIdentifier as createExtId, createMainContextProxyIdentifier as createMainId, IRPCProtocol } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import * as search from 'vs/workbench/services/search/common/search'; import { SaveReason } from 'vs/workbench/common/editor'; @@ -51,7 +51,7 @@ import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { TunnelOptions } from 'vs/platform/remote/common/tunnel'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; -import { INotebookMimeTypeSelector, IProcessedOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent, NotebookDataDto, INotebookKernelInfoDto, IMainCellDto, IOutputRenderRequest, IOutputRenderResponse, INotebookDocumentFilter, INotebookKernelInfoDto2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IProcessedOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent, NotebookDataDto, IMainCellDto, INotebookDocumentFilter, INotebookKernelInfoDto2, TransientMetadata, INotebookCellStatusBarEntry, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { Dto } from 'vs/base/common/types'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; @@ -448,10 +448,9 @@ export interface MainThreadTerminalServiceShape extends IDisposable { $show(terminalId: number, preserveFocus: boolean): void; $startSendingDataEvents(): void; $stopSendingDataEvents(): void; - $startHandlingLinks(): void; - $stopHandlingLinks(): void; $startLinkProvider(): void; $stopLinkProvider(): void; + $registerProcessSupport(isSupported: boolean): void; $setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: ISerializableEnvironmentVariableCollection | undefined): void; // Process @@ -583,7 +582,7 @@ export interface ExtHostEditorInsetsShape { $onDidReceiveMessage(handle: number, message: any): void; } -export type WebviewPanelHandle = string; +export type WebviewHandle = string; export interface WebviewPanelShowOptions { readonly viewColumn?: EditorViewColumn; @@ -611,20 +610,23 @@ export interface CustomTextEditorCapabilities { } export interface MainThreadWebviewsShape extends IDisposable { - $createWebviewPanel(extension: WebviewExtensionDescription, handle: WebviewPanelHandle, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: modes.IWebviewPanelOptions & modes.IWebviewOptions): void; - $disposeWebview(handle: WebviewPanelHandle): void; - $reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void; - $setTitle(handle: WebviewPanelHandle, value: string): void; - $setIconPath(handle: WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void; + $setHtml(handle: WebviewHandle, value: string): void; + $setOptions(handle: WebviewHandle, options: modes.IWebviewOptions): void; + $postMessage(handle: WebviewHandle, value: any): Promise +} - $setHtml(handle: WebviewPanelHandle, value: string): void; - $setOptions(handle: WebviewPanelHandle, options: modes.IWebviewOptions): void; - - $postMessage(handle: WebviewPanelHandle, value: any): Promise; +export interface MainThreadWebviewPanelsShape extends IDisposable { + $createWebviewPanel(extension: WebviewExtensionDescription, handle: WebviewHandle, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: modes.IWebviewPanelOptions & modes.IWebviewOptions): void; + $disposeWebview(handle: WebviewHandle): void; + $reveal(handle: WebviewHandle, showOptions: WebviewPanelShowOptions): void; + $setTitle(handle: WebviewHandle, value: string): void; + $setIconPath(handle: WebviewHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void; $registerSerializer(viewType: string): void; $unregisterSerializer(viewType: string): void; +} +export interface MainThreadCustomEditorsShape extends IDisposable { $registerTextEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: CustomTextEditorCapabilities): void; $registerCustomEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean): void; $unregisterEditorProvider(viewType: string): void; @@ -633,6 +635,13 @@ export interface MainThreadWebviewsShape extends IDisposable { $onContentChange(resource: UriComponents, viewType: string): void; } +export interface MainThreadWebviewViewsShape extends IDisposable { + $registerWebviewViewProvider(viewType: string, options?: { retainContextWhenHidden?: boolean }): void; + $unregisterWebviewViewProvider(viewType: string): void; + + $setWebviewViewTitle(handle: WebviewHandle, value: string | undefined): void; +} + export interface WebviewPanelViewStateData { [handle: string]: { readonly active: boolean; @@ -642,14 +651,18 @@ export interface WebviewPanelViewStateData { } export interface ExtHostWebviewsShape { - $onMessage(handle: WebviewPanelHandle, message: any): void; - $onMissingCsp(handle: WebviewPanelHandle, extensionId: string): void; + $onMessage(handle: WebviewHandle, message: any): void; + $onMissingCsp(handle: WebviewHandle, extensionId: string): void; +} + +export interface ExtHostWebviewPanelsShape { $onDidChangeWebviewPanelViewStates(newState: WebviewPanelViewStateData): void; - $onDidDisposeWebviewPanel(handle: WebviewPanelHandle): Promise; + $onDidDisposeWebviewPanel(handle: WebviewHandle): Promise; + $deserializeWebviewPanel(newWebviewHandle: WebviewHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise; +} - $deserializeWebviewPanel(newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise; - - $resolveWebviewEditor(resource: UriComponents, newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, cancellation: CancellationToken): Promise; +export interface ExtHostCustomEditorsShape { + $resolveWebviewEditor(resource: UriComponents, newWebviewHandle: WebviewHandle, viewType: string, title: string, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, cancellation: CancellationToken): Promise; $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, cancellation: CancellationToken): Promise<{ editable: boolean }>; $disposeCustomDocument(resource: UriComponents, viewType: string): Promise; @@ -663,7 +676,15 @@ export interface ExtHostWebviewsShape { $backup(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise; - $onMoveCustomEditor(handle: WebviewPanelHandle, newResource: UriComponents, viewType: string): Promise; + $onMoveCustomEditor(handle: WebviewHandle, newResource: UriComponents, viewType: string): Promise; +} + +export interface ExtHostWebviewViewsShape { + $resolveWebviewView(webviewHandle: WebviewHandle, viewType: string, state: any, cancellation: CancellationToken): Promise; + + $onDidChangeWebviewViewVisibility(webviewHandle: WebviewHandle, visible: boolean): void; + + $disposeWebviewView(webviewHandle: WebviewHandle): void; } export enum CellKind { @@ -699,24 +720,29 @@ export type NotebookCellOutputsSplice = [ IProcessedOutput[] ]; +export enum NotebookEditorRevealType { + Default = 0, + InCenter = 1, + InCenterIfOutsideViewport = 2, +} + +export type INotebookCellStatusBarEntryDto = Dto; + export interface MainThreadNotebookShape extends IDisposable { - $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, supportBackup: boolean, kernelInfoDto: INotebookKernelInfoDto | undefined): Promise; + $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, supportBackup: boolean, options: { transientOutputs: boolean; transientMetadata: TransientMetadata }): Promise; $onNotebookChange(viewType: string, resource: UriComponents): Promise; $unregisterNotebookProvider(viewType: string): Promise; - $registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: UriComponents[]): Promise; - $unregisterNotebookRenderer(id: string): Promise; - $registerNotebookKernel(extension: NotebookExtensionDescription, id: string, label: string, selectors: (string | IRelativePattern)[], preloads: UriComponents[]): Promise; $registerNotebookKernelProvider(extension: NotebookExtensionDescription, handle: number, documentFilter: INotebookDocumentFilter): Promise; $unregisterNotebookKernelProvider(handle: number): Promise; - $onNotebookKernelChange(handle: number): void; - $unregisterNotebookKernel(id: string): Promise; - $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[], renderers: number[]): Promise; + $onNotebookKernelChange(handle: number, uri: UriComponents | undefined): void; + $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[]): Promise; $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise; $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata): Promise; $updateNotebookCellMetadata(viewType: string, resource: UriComponents, handle: number, metadata: NotebookCellMetadata | undefined): Promise; - $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise; + $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[]): Promise; $postMessage(editorId: string, forRendererId: string | undefined, value: any): Promise; - + $setStatusBarEntry(id: number, statusBarEntry: INotebookCellStatusBarEntryDto): Promise; + $tryRevealRange(id: string, range: ICellRange, revealType: NotebookEditorRevealType): Promise; $onDidEdit(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void; $onContentChange(resource: UriComponents, viewType: string): void; } @@ -881,6 +907,7 @@ export interface MainThreadDebugServiceShape extends IDisposable { $stopDebugging(sessionId: DebugSessionUUID | undefined): Promise; $setDebugSessionName(id: DebugSessionUUID, name: string): void; $customDebugAdapterRequest(id: DebugSessionUUID, command: string, args: any): Promise; + $getDebugProtocolBreakpoint(id: DebugSessionUUID, breakpoinId: string): Promise; $appendDebugConsole(value: string): void; $startBreakpointEvents(): void; $registerBreakpoints(breakpoints: Array): Promise; @@ -1033,6 +1060,7 @@ export interface ExtHostAuthenticationShape { $logout(id: string, sessionId: string): Promise; $onDidChangeAuthenticationSessions(id: string, label: string, event: modes.AuthenticationSessionsChangeEvent): Promise; $onDidChangeAuthenticationProviders(added: modes.AuthenticationProviderInformation[], removed: modes.AuthenticationProviderInformation[]): Promise; + $setProviders(providers: modes.AuthenticationProviderInformation[]): Promise; } export interface ExtHostSearchShape { @@ -1060,7 +1088,7 @@ export type IResolveAuthorityResult = IResolveAuthorityErrorResult | IResolveAut export interface ExtHostExtensionServiceShape { $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise; $startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise; - $activateByEvent(activationEvent: string): Promise; + $activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise; $activate(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise; $setRemoteEnvironment(env: { [key: string]: string | null; }): Promise; $updateRemoteConnectionData(connectionData: IRemoteConnectionData): Promise; @@ -1218,7 +1246,14 @@ export interface IWorkspaceEditEntryMetadataDto { iconPath?: { id: string } | UriComponents | { light: UriComponents, dark: UriComponents }; } +export const enum WorkspaceEditType { + File = 1, + Text = 2, + Cell = 3, +} + export interface IWorkspaceFileEditDto { + _type: WorkspaceEditType.File; oldUri?: UriComponents; newUri?: UriComponents; options?: modes.WorkspaceFileEditOptions @@ -1226,14 +1261,23 @@ export interface IWorkspaceFileEditDto { } export interface IWorkspaceTextEditDto { + _type: WorkspaceEditType.Text; resource: UriComponents; edit: modes.TextEdit; modelVersionId?: number; metadata?: IWorkspaceEditEntryMetadataDto; } +export interface IWorkspaceCellEditDto { + _type: WorkspaceEditType.Cell; + resource: UriComponents; + edit: ICellEditOperation; + modelVersionId?: number; + metadata?: IWorkspaceEditEntryMetadataDto; +} + export interface IWorkspaceEditDto { - edits: Array; + edits: Array; // todo@joh reject should go into rename rejectReason?: string; @@ -1334,7 +1378,7 @@ export interface ExtHostLanguageFeaturesShape { $provideHover(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideEvaluatableExpression(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideDocumentHighlights(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; - $provideOnTypeRenameRanges(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; + $provideOnTypeRenameRanges(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise<{ ranges: IRange[]; wordPattern?: IRegExpDto; } | undefined>; $provideReferences(handle: number, resource: UriComponents, position: IPosition, context: modes.ReferenceContext, token: CancellationToken): Promise; $provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext, token: CancellationToken): Promise; $releaseCodeActions(handle: number, cacheId: number): void; @@ -1434,7 +1478,6 @@ export interface ExtHostTerminalServiceShape { $acceptWorkspacePermissionsChanged(isAllowed: boolean): void; $getAvailableShells(): Promise; $getDefaultShellAndArgs(useAutomationShell: boolean): Promise; - $handleLink(id: number, link: string): Promise; $provideLinks(id: number, line: string): Promise; $activateLink(id: number, linkId: number): void; $initEnvironmentVariableCollections(collections: [string, ISerializableEnvironmentVariableCollection][]): void; @@ -1451,7 +1494,7 @@ export interface ExtHostSCMShape { export interface ExtHostTaskShape { $provideTasks(handle: number, validTypes: { [key: string]: boolean; }): Thenable; $resolveTask(handle: number, taskDTO: tasks.TaskDTO): Thenable; - $onDidStartTask(execution: tasks.TaskExecutionDTO, terminalId: number): void; + $onDidStartTask(execution: tasks.TaskExecutionDTO, terminalId: number, resolvedDefinition: tasks.TaskDefinitionDTO): void; $onDidStartTaskProcess(value: tasks.TaskProcessStartedDTO): void; $onDidEndTaskProcess(value: tasks.TaskProcessEndedDTO): void; $OnDidEndTask(execution: tasks.TaskExecutionDTO): void; @@ -1585,9 +1628,22 @@ export interface INotebookSelectionChangeEvent { selections: number[]; } +export interface INotebookCellVisibleRange { + start: number; + end: number; +} + +export interface INotebookVisibleRangesEvent { + ranges: INotebookCellVisibleRange[]; +} + export interface INotebookEditorPropertiesChangeData { - selections: INotebookSelectionChangeEvent | null; + visibleRanges: INotebookVisibleRangesEvent | null; +} + +export interface INotebookDocumentPropertiesChangeData { metadata: NotebookDocumentMetadata | null; + selections: INotebookSelectionChangeEvent | null; } export interface INotebookModelAddedData { @@ -1597,13 +1653,14 @@ export interface INotebookModelAddedData { cells: IMainCellDto[], viewType: string; metadata?: NotebookDocumentMetadata; - attachedEditor?: { id: string; selections: number[]; } + attachedEditor?: { id: string; selections: number[]; visibleRanges: ICellRange[] } } export interface INotebookEditorAddData { id: string; documentUri: UriComponents; selections: number[]; + visibleRanges: ICellRange[]; } export interface INotebookDocumentsAndEditorsDelta { @@ -1620,8 +1677,6 @@ export interface ExtHostNotebookShape { $resolveNotebookEditor(viewType: string, uri: UriComponents, editorId: string): Promise; $provideNotebookKernels(handle: number, uri: UriComponents, token: CancellationToken): Promise; $resolveNotebookKernel(handle: number, editorId: string, uri: UriComponents, kernelId: string, token: CancellationToken): Promise; - $executeNotebookByAttachedKernel(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise; - $cancelNotebookByAttachedKernel(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise; $executeNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined): Promise; $cancelNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined): Promise; $executeNotebook2(kernelId: string, viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise; @@ -1630,13 +1685,12 @@ export interface ExtHostNotebookShape { $backup(viewType: string, uri: UriComponents, cancellation: CancellationToken): Promise; $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void; $acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelId: string | undefined }): void; - $renderOutputs(uriComponents: UriComponents, id: string, request: IOutputRenderRequest): Promise | undefined>; - $renderOutputs2(uriComponents: UriComponents, id: string, request: IOutputRenderRequest): Promise | undefined>; $onDidReceiveMessage(editorId: string, rendererId: string | undefined, message: unknown): void; - $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent): void; + $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent, isDirty: boolean): void; $acceptModelSaved(uriComponents: UriComponents): void; - $acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void; - $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): Promise; + $acceptEditorPropertiesChanged(id: string, data: INotebookEditorPropertiesChangeData): void; + $acceptDocumentPropertiesChanged(uriComponents: UriComponents, data: INotebookDocumentPropertiesChangeData): void; + $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): void; $undoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise; $redoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise; @@ -1698,6 +1752,9 @@ export const MainContext = { MainThreadTelemetry: createMainId('MainThreadTelemetry'), MainThreadTerminalService: createMainId('MainThreadTerminalService'), MainThreadWebviews: createMainId('MainThreadWebviews'), + MainThreadWebviewPanels: createMainId('MainThreadWebviewPanels'), + MainThreadWebviewViews: createMainId('MainThreadWebviewViews'), + MainThreadCustomEditors: createMainId('MainThreadCustomEditors'), MainThreadUrls: createMainId('MainThreadUrls'), MainThreadWorkspace: createMainId('MainThreadWorkspace'), MainThreadFileSystem: createMainId('MainThreadFileSystem'), @@ -1738,6 +1795,9 @@ export const ExtHostContext = { ExtHostWorkspace: createExtId('ExtHostWorkspace'), ExtHostWindow: createExtId('ExtHostWindow'), ExtHostWebviews: createExtId('ExtHostWebviews'), + ExtHostWebviewPanels: createExtId('ExtHostWebviewPanels'), + ExtHostCustomEditors: createExtId('ExtHostCustomEditors'), + ExtHostWebviewViews: createExtId('ExtHostWebviewViews'), ExtHostEditorInsets: createExtId('ExtHostEditorInsets'), ExtHostProgress: createMainId('ExtHostProgress'), ExtHostComments: createMainId('ExtHostComments'), diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index da8a6820ca0..c7dd0f2d5b2 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -28,6 +28,11 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { this._proxy = mainContext.getProxy(MainContext.MainThreadAuthentication); } + $setProviders(providers: vscode.AuthenticationProviderInformation[]): Promise { + this._providers = providers; + return Promise.resolve(); + } + getProviderIds(): Promise> { return this._proxy.$getProviderIds(); } diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index 83fc7bc31ac..8dc69cb7abd 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -141,7 +141,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { try { const result = await this._proxy.$executeCommand(id, toArgs, retry); - return revive(result); + return revive(result); } catch (e) { // Rerun the command when it wasn't known, had arguments, and when retry // is enabled. We do this because the command might be registered inside diff --git a/src/vs/workbench/api/common/extHostCustomEditors.ts b/src/vs/workbench/api/common/extHostCustomEditors.ts new file mode 100644 index 00000000000..f3b3cbd8438 --- /dev/null +++ b/src/vs/workbench/api/common/extHostCustomEditors.ts @@ -0,0 +1,388 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { hash } from 'vs/base/common/hash'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { joinPath } from 'vs/base/common/resources'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import * as modes from 'vs/editor/common/modes'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; +import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; +import { ExtHostWebviews, toExtensionData } from 'vs/workbench/api/common/extHostWebview'; +import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; +import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; +import type * as vscode from 'vscode'; +import { Cache } from './cache'; +import * as extHostProtocol from './extHost.protocol'; +import * as extHostTypes from './extHostTypes'; + + +class CustomDocumentStoreEntry { + + private _backupCounter = 1; + + constructor( + public readonly document: vscode.CustomDocument, + private readonly _storagePath: URI | undefined, + ) { } + + private readonly _edits = new Cache('custom documents'); + + private _backup?: vscode.CustomDocumentBackup; + + addEdit(item: vscode.CustomDocumentEditEvent): number { + return this._edits.add([item]); + } + + async undo(editId: number, isDirty: boolean): Promise { + await this.getEdit(editId).undo(); + if (!isDirty) { + this.disposeBackup(); + } + } + + async redo(editId: number, isDirty: boolean): Promise { + await this.getEdit(editId).redo(); + if (!isDirty) { + this.disposeBackup(); + } + } + + disposeEdits(editIds: number[]): void { + for (const id of editIds) { + this._edits.delete(id); + } + } + + getNewBackupUri(): URI { + if (!this._storagePath) { + throw new Error('Backup requires a valid storage path'); + } + const fileName = hashPath(this.document.uri) + (this._backupCounter++); + return joinPath(this._storagePath, fileName); + } + + updateBackup(backup: vscode.CustomDocumentBackup): void { + this._backup?.delete(); + this._backup = backup; + } + + disposeBackup(): void { + this._backup?.delete(); + this._backup = undefined; + } + + private getEdit(editId: number): vscode.CustomDocumentEditEvent { + const edit = this._edits.get(editId, 0); + if (!edit) { + throw new Error('No edit found'); + } + return edit; + } +} + +class CustomDocumentStore { + private readonly _documents = new Map(); + + public get(viewType: string, resource: vscode.Uri): CustomDocumentStoreEntry | undefined { + return this._documents.get(this.key(viewType, resource)); + } + + public add(viewType: string, document: vscode.CustomDocument, storagePath: URI | undefined): CustomDocumentStoreEntry { + const key = this.key(viewType, document.uri); + if (this._documents.has(key)) { + throw new Error(`Document already exists for viewType:${viewType} resource:${document.uri}`); + } + const entry = new CustomDocumentStoreEntry(document, storagePath); + this._documents.set(key, entry); + return entry; + } + + public delete(viewType: string, document: vscode.CustomDocument) { + const key = this.key(viewType, document.uri); + this._documents.delete(key); + } + + private key(viewType: string, resource: vscode.Uri): string { + return `${viewType}@@@${resource}`; + } + +} + +const enum WebviewEditorType { + Text, + Custom +} + +type ProviderEntry = { + readonly extension: IExtensionDescription; + readonly type: WebviewEditorType.Text; + readonly provider: vscode.CustomTextEditorProvider; +} | { + readonly extension: IExtensionDescription; + readonly type: WebviewEditorType.Custom; + readonly provider: vscode.CustomReadonlyEditorProvider; +}; + +class EditorProviderStore { + private readonly _providers = new Map(); + + public addTextProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider): vscode.Disposable { + return this.add(WebviewEditorType.Text, viewType, extension, provider); + } + + public addCustomProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomReadonlyEditorProvider): vscode.Disposable { + return this.add(WebviewEditorType.Custom, viewType, extension, provider); + } + + public get(viewType: string): ProviderEntry | undefined { + return this._providers.get(viewType); + } + + private add(type: WebviewEditorType, viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider | vscode.CustomReadonlyEditorProvider): 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); + return new extHostTypes.Disposable(() => this._providers.delete(viewType)); + } +} + +export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditorsShape { + + private readonly _proxy: extHostProtocol.MainThreadCustomEditorsShape; + + private readonly _editorProviders = new EditorProviderStore(); + + private readonly _documents = new CustomDocumentStore(); + + constructor( + mainContext: extHostProtocol.IMainContext, + private readonly _extHostDocuments: ExtHostDocuments, + private readonly _extensionStoragePaths: IExtensionStoragePaths | undefined, + private readonly _extHostWebview: ExtHostWebviews, + private readonly _extHostWebviewPanels: ExtHostWebviewPanels, + ) { + this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadCustomEditors); + } + + public registerCustomEditorProvider( + extension: IExtensionDescription, + viewType: string, + provider: vscode.CustomReadonlyEditorProvider | vscode.CustomTextEditorProvider, + options: { webviewOptions?: vscode.WebviewPanelOptions, supportsMultipleEditorsPerDocument?: boolean }, + ): vscode.Disposable { + const disposables = new DisposableStore(); + if ('resolveCustomTextEditor' in provider) { + disposables.add(this._editorProviders.addTextProvider(viewType, extension, provider)); + this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, { + supportsMove: !!provider.moveCustomTextEditor, + }); + } else { + disposables.add(this._editorProviders.addCustomProvider(viewType, extension, provider)); + + if (this.supportEditing(provider)) { + disposables.add(provider.onDidChangeCustomDocument(e => { + const entry = this.getCustomDocumentEntry(viewType, e.document.uri); + if (isEditEvent(e)) { + const editId = entry.addEdit(e); + this._proxy.$onDidEdit(e.document.uri, viewType, editId, e.label); + } else { + this._proxy.$onContentChange(e.document.uri, viewType); + } + })); + } + + this._proxy.$registerCustomEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, !!options.supportsMultipleEditorsPerDocument); + } + + return extHostTypes.Disposable.from( + disposables, + new extHostTypes.Disposable(() => { + this._proxy.$unregisterEditorProvider(viewType); + })); + } + + + async $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, cancellation: CancellationToken) { + const entry = this._editorProviders.get(viewType); + if (!entry) { + throw new Error(`No provider found for '${viewType}'`); + } + + if (entry.type !== WebviewEditorType.Custom) { + throw new Error(`Invalid provide type for '${viewType}'`); + } + + const revivedResource = URI.revive(resource); + const document = await entry.provider.openCustomDocument(revivedResource, { backupId }, cancellation); + + let storageRoot: URI | undefined; + if (this.supportEditing(entry.provider) && this._extensionStoragePaths) { + storageRoot = this._extensionStoragePaths.workspaceValue(entry.extension) ?? this._extensionStoragePaths.globalValue(entry.extension); + } + this._documents.add(viewType, document, storageRoot); + + return { editable: this.supportEditing(entry.provider) }; + } + + async $disposeCustomDocument(resource: UriComponents, viewType: string): Promise { + const entry = this._editorProviders.get(viewType); + if (!entry) { + throw new Error(`No provider found for '${viewType}'`); + } + + if (entry.type !== WebviewEditorType.Custom) { + throw new Error(`Invalid provider type for '${viewType}'`); + } + + const revivedResource = URI.revive(resource); + const { document } = this.getCustomDocumentEntry(viewType, revivedResource); + this._documents.delete(viewType, document); + document.dispose(); + } + + async $resolveWebviewEditor( + resource: UriComponents, + handle: extHostProtocol.WebviewHandle, + viewType: string, + title: string, + position: EditorViewColumn, + options: modes.IWebviewOptions & modes.IWebviewPanelOptions, + cancellation: CancellationToken, + ): Promise { + const entry = this._editorProviders.get(viewType); + if (!entry) { + throw new Error(`No provider found for '${viewType}'`); + } + + const webview = this._extHostWebview.createNewWebview(handle, options, entry.extension); + const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, title, position, options, webview); + + const revivedResource = URI.revive(resource); + + switch (entry.type) { + case WebviewEditorType.Custom: + { + const { document } = this.getCustomDocumentEntry(viewType, revivedResource); + return entry.provider.resolveCustomEditor(document, panel, cancellation); + } + case WebviewEditorType.Text: + { + const document = this._extHostDocuments.getDocument(revivedResource); + return entry.provider.resolveCustomTextEditor(document, panel, cancellation); + } + default: + { + throw new Error('Unknown webview provider type'); + } + } + } + + $disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void { + const document = this.getCustomDocumentEntry(viewType, resourceComponents); + document.disposeEdits(editIds); + } + + async $onMoveCustomEditor(handle: string, newResourceComponents: UriComponents, viewType: string): Promise { + const entry = this._editorProviders.get(viewType); + if (!entry) { + throw new Error(`No provider found for '${viewType}'`); + } + + if (!(entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor) { + throw new Error(`Provider does not implement move '${viewType}'`); + } + + const webview = this._extHostWebviewPanels.getWebviewPanel(handle); + if (!webview) { + throw new Error(`No webview found`); + } + + const resource = URI.revive(newResourceComponents); + const document = this._extHostDocuments.getDocument(resource); + await (entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor!(document, webview, CancellationToken.None); + } + + async $undo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise { + const entry = this.getCustomDocumentEntry(viewType, resourceComponents); + return entry.undo(editId, isDirty); + } + + async $redo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise { + const entry = this.getCustomDocumentEntry(viewType, resourceComponents); + return entry.redo(editId, isDirty); + } + + async $revert(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { + const entry = this.getCustomDocumentEntry(viewType, resourceComponents); + const provider = this.getCustomEditorProvider(viewType); + await provider.revertCustomDocument(entry.document, cancellation); + entry.disposeBackup(); + } + + async $onSave(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { + const entry = this.getCustomDocumentEntry(viewType, resourceComponents); + const provider = this.getCustomEditorProvider(viewType); + await provider.saveCustomDocument(entry.document, cancellation); + entry.disposeBackup(); + } + + async $onSaveAs(resourceComponents: UriComponents, viewType: string, targetResource: UriComponents, cancellation: CancellationToken): Promise { + const entry = this.getCustomDocumentEntry(viewType, resourceComponents); + const provider = this.getCustomEditorProvider(viewType); + return provider.saveCustomDocumentAs(entry.document, URI.revive(targetResource), cancellation); + } + + async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { + const entry = this.getCustomDocumentEntry(viewType, resourceComponents); + const provider = this.getCustomEditorProvider(viewType); + + const backup = await provider.backupCustomDocument(entry.document, { + destination: entry.getNewBackupUri(), + }, cancellation); + entry.updateBackup(backup); + return backup.id; + } + + + private getCustomDocumentEntry(viewType: string, resource: UriComponents): CustomDocumentStoreEntry { + const entry = this._documents.get(viewType, URI.revive(resource)); + if (!entry) { + throw new Error('No custom document found'); + } + return entry; + } + + private getCustomEditorProvider(viewType: string): vscode.CustomEditorProvider { + const entry = this._editorProviders.get(viewType); + const provider = entry?.provider; + if (!provider || !this.supportEditing(provider)) { + throw new Error('Custom document is not editable'); + } + return provider; + } + + private supportEditing( + provider: vscode.CustomTextEditorProvider | vscode.CustomEditorProvider | vscode.CustomReadonlyEditorProvider + ): provider is vscode.CustomEditorProvider { + return !!(provider as vscode.CustomEditorProvider).onDidChangeCustomDocument; + } +} + + +function isEditEvent(e: vscode.CustomDocumentContentChangeEvent | vscode.CustomDocumentEditEvent): e is vscode.CustomDocumentEditEvent { + return typeof (e as vscode.CustomDocumentEditEvent).undo === 'function' + && typeof (e as vscode.CustomDocumentEditEvent).redo === 'function'; +} + +function hashPath(resource: URI): string { + const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString(); + return hash(str) + ''; +} + diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index 82121bcb2c6..1e1643ee414 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -11,12 +11,12 @@ import { MainContext, MainThreadDebugServiceShape, ExtHostDebugServiceShape, DebugSessionUUID, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto } from 'vs/workbench/api/common/extHost.protocol'; -import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint, DebugAdapterServer, DebugAdapterExecutable, DataBreakpoint, DebugConsoleMode, DebugAdapterInlineImplementation } from 'vs/workbench/api/common/extHostTypes'; +import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint, DebugAdapterServer, DebugAdapterExecutable, DataBreakpoint, DebugConsoleMode, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer } from 'vs/workbench/api/common/extHostTypes'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { ExtHostDocumentsAndEditors, IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { IDebuggerContribution, IConfig, IDebugAdapter, IDebugAdapterServer, IDebugAdapterExecutable, IAdapterDescriptor, IDebugAdapterImpl } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebuggerContribution, IConfig, IDebugAdapter, IDebugAdapterServer, IDebugAdapterExecutable, IAdapterDescriptor, IDebugAdapterImpl, IDebugAdapterNamedPipeServer } from 'vs/workbench/contrib/debug/common/debug'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; import { ExtHostConfigProvider, IExtHostConfiguration } from '../common/extHostConfiguration'; @@ -737,6 +737,11 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E port: x.port, host: x.host }; + } else if (x instanceof DebugAdapterNamedPipeServer) { + return { + type: 'pipeServer', + path: x.path + }; } else if (x instanceof DebugAdapterInlineImplementation) { return { type: 'implementation', @@ -957,6 +962,10 @@ export class ExtHostDebugSession implements vscode.DebugSession { public customRequest(command: string, args: any): Promise { return this._debugServiceProxy.$customDebugAdapterRequest(this._id, command, args); } + + public getDebugProtocolBreakpoint(breakpoint: vscode.Breakpoint): Promise { + return this._debugServiceProxy.$getDebugProtocolBreakpoint(this._id, breakpoint.id); + } } export class ExtHostDebugConsole implements vscode.DebugConsole { diff --git a/src/vs/workbench/api/common/extHostDecorations.ts b/src/vs/workbench/api/common/extHostDecorations.ts index d50d8f15dd2..3e52bbcc3aa 100644 --- a/src/vs/workbench/api/common/extHostDecorations.ts +++ b/src/vs/workbench/api/common/extHostDecorations.ts @@ -19,7 +19,7 @@ interface ProviderData { extensionId: ExtensionIdentifier; } -export class ExtHostDecorations implements IExtHostDecorations { +export class ExtHostDecorations implements ExtHostDecorationsShape { private static _handlePool = 0; @@ -85,4 +85,4 @@ export class ExtHostDecorations implements IExtHostDecorations { } export const IExtHostDecorations = createDecorator('IExtHostDecorations'); -export interface IExtHostDecorations extends ExtHostDecorations, ExtHostDecorationsShape { } +export interface IExtHostDecorations extends ExtHostDecorations { } diff --git a/src/vs/workbench/api/common/extHostDocumentData.ts b/src/vs/workbench/api/common/extHostDocumentData.ts index 756f1a03f84..0cd5c8378fc 100644 --- a/src/vs/workbench/api/common/extHostDocumentData.ts +++ b/src/vs/workbench/api/common/extHostDocumentData.ts @@ -29,19 +29,17 @@ export function getWordDefinitionFor(modeId: string): RegExp | undefined { export class ExtHostDocumentData extends MirrorTextModel { - private _proxy: MainThreadDocumentsShape; - private _languageId: string; - private _isDirty: boolean; private _document?: vscode.TextDocument; private _isDisposed: boolean = false; - constructor(proxy: MainThreadDocumentsShape, uri: URI, lines: string[], eol: string, - languageId: string, versionId: number, isDirty: boolean + constructor( + private readonly _proxy: MainThreadDocumentsShape, + uri: URI, lines: string[], eol: string, versionId: number, + private _languageId: string, + private _isDirty: boolean, + private readonly _notebook?: vscode.NotebookDocument | undefined ) { super(uri, lines, eol, versionId); - this._proxy = proxy; - this._languageId = languageId; - this._isDirty = isDirty; } dispose(): void { @@ -59,25 +57,26 @@ export class ExtHostDocumentData extends MirrorTextModel { get document(): vscode.TextDocument { if (!this._document) { - const data = this; + const that = this; this._document = { - get uri() { return data._uri; }, - get fileName() { return data._uri.fsPath; }, - get isUntitled() { return data._uri.scheme === Schemas.untitled; }, - get languageId() { return data._languageId; }, - get version() { return data._versionId; }, - get isClosed() { return data._isDisposed; }, - get isDirty() { return data._isDirty; }, - save() { return data._save(); }, - getText(range?) { return range ? data._getTextInRange(range) : data.getText(); }, - get eol() { return data._eol === '\n' ? EndOfLine.LF : EndOfLine.CRLF; }, - get lineCount() { return data._lines.length; }, - lineAt(lineOrPos: number | vscode.Position) { return data._lineAt(lineOrPos); }, - offsetAt(pos) { return data._offsetAt(pos); }, - positionAt(offset) { return data._positionAt(offset); }, - validateRange(ran) { return data._validateRange(ran); }, - validatePosition(pos) { return data._validatePosition(pos); }, - getWordRangeAtPosition(pos, regexp?) { return data._getWordRangeAtPosition(pos, regexp); } + get uri() { return that._uri; }, + get fileName() { return that._uri.fsPath; }, + get isUntitled() { return that._uri.scheme === Schemas.untitled; }, + get languageId() { return that._languageId; }, + get version() { return that._versionId; }, + get isClosed() { return that._isDisposed; }, + get isDirty() { return that._isDirty; }, + get notebook() { return that._notebook; }, + save() { return that._save(); }, + getText(range?) { return range ? that._getTextInRange(range) : that.getText(); }, + get eol() { return that._eol === '\n' ? EndOfLine.LF : EndOfLine.CRLF; }, + get lineCount() { return that._lines.length; }, + lineAt(lineOrPos: number | vscode.Position) { return that._lineAt(lineOrPos); }, + offsetAt(pos) { return that._offsetAt(pos); }, + positionAt(offset) { return that._positionAt(offset); }, + validateRange(ran) { return that._validateRange(ran); }, + validatePosition(pos) { return that._validatePosition(pos); }, + getWordRangeAtPosition(pos, regexp?) { return that._getWordRangeAtPosition(pos, regexp); }, }; } return Object.freeze(this._document); diff --git a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts index 1eb5b7bc391..5613e66db18 100644 --- a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts +++ b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts @@ -6,7 +6,7 @@ import { Event } from 'vs/base/common/event'; import { URI, UriComponents } from 'vs/base/common/uri'; import { illegalState } from 'vs/base/common/errors'; -import { ExtHostDocumentSaveParticipantShape, MainThreadTextEditorsShape, IWorkspaceEditDto } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostDocumentSaveParticipantShape, MainThreadTextEditorsShape, IWorkspaceEditDto, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol'; import { TextEdit } from 'vs/workbench/api/common/extHostTypes'; import { Range, TextDocumentSaveReason, EndOfLine } from 'vs/workbench/api/common/extHostTypeConverters'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; @@ -146,6 +146,7 @@ export class ExtHostDocumentSaveParticipant implements ExtHostDocumentSavePartic if (Array.isArray(value) && (value).every(e => e instanceof TextEdit)) { for (const { newText, newEol, range } of value) { dto.edits.push({ + _type: WorkspaceEditType.Text, resource: document.uri, edit: { range: range && Range.from(range), diff --git a/src/vs/workbench/api/common/extHostDocuments.ts b/src/vs/workbench/api/common/extHostDocuments.ts index af4a3f9de52..033b4262226 100644 --- a/src/vs/workbench/api/common/extHostDocuments.ts +++ b/src/vs/workbench/api/common/extHostDocuments.ts @@ -53,7 +53,7 @@ export class ExtHostDocuments implements ExtHostDocumentsShape { } public getAllDocumentData(): ExtHostDocumentData[] { - return this._documentsAndEditors.allDocuments(); + return [...this._documentsAndEditors.allDocuments()]; } public getDocumentData(resource: vscode.Uri): ExtHostDocumentData | undefined { @@ -69,8 +69,8 @@ export class ExtHostDocuments implements ExtHostDocumentsShape { public getDocument(resource: vscode.Uri): vscode.TextDocument { const data = this.getDocumentData(resource); - if (!data || !data.document) { - throw new Error('Unable to retrieve document from URI'); + if (!data?.document) { + throw new Error(`Unable to retrieve document from URI '${resource}'`); } return data.document; } diff --git a/src/vs/workbench/api/common/extHostDocumentsAndEditors.ts b/src/vs/workbench/api/common/extHostDocumentsAndEditors.ts index bf3919a0841..e378b49a198 100644 --- a/src/vs/workbench/api/common/extHostDocumentsAndEditors.ts +++ b/src/vs/workbench/api/common/extHostDocumentsAndEditors.ts @@ -4,17 +4,39 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'vs/base/common/assert'; +import * as vscode from 'vscode'; import { Emitter, Event } from 'vs/base/common/event'; import { dispose } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ExtHostDocumentsAndEditorsShape, IDocumentsAndEditorsDelta, MainContext } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostDocumentsAndEditorsShape, IDocumentsAndEditorsDelta, IModelAddedData, MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostTextEditor } from 'vs/workbench/api/common/extHostTextEditor'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { ILogService } from 'vs/platform/log/common/log'; import { ResourceMap } from 'vs/base/common/map'; +import { Schemas } from 'vs/base/common/network'; +import { Iterable } from 'vs/base/common/iterator'; + +class Reference { + private _count = 0; + constructor(readonly value: T) { } + ref() { + this._count++; + } + unref() { + return --this._count === 0; + } +} + +export interface IExtHostModelAddedData extends IModelAddedData { + notebook?: vscode.NotebookDocument; +} + +export interface IExtHostDocumentsAndEditorsDelta extends IDocumentsAndEditorsDelta { + addedDocuments?: IExtHostModelAddedData[]; +} export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsShape { @@ -23,7 +45,7 @@ export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsSha private _activeEditorId: string | null = null; private readonly _editors = new Map(); - private readonly _documents = new ResourceMap(); + private readonly _documents = new ResourceMap>(); private readonly _onDidAddDocuments = new Emitter(); private readonly _onDidRemoveDocuments = new Emitter(); @@ -41,6 +63,10 @@ export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsSha ) { } $acceptDocumentsAndEditorsDelta(delta: IDocumentsAndEditorsDelta): void { + this.acceptDocumentsAndEditorsDelta(delta); + } + + acceptDocumentsAndEditorsDelta(delta: IExtHostDocumentsAndEditorsDelta): void { const removedDocuments: ExtHostDocumentData[] = []; const addedDocuments: ExtHostDocumentData[] = []; @@ -50,9 +76,9 @@ export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsSha for (const uriComponent of delta.removedDocuments) { const uri = URI.revive(uriComponent); const data = this._documents.get(uri); - this._documents.delete(uri); - if (data) { - removedDocuments.push(data); + if (data?.unref()) { + this._documents.delete(uri); + removedDocuments.push(data.value); } } } @@ -60,19 +86,31 @@ export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsSha if (delta.addedDocuments) { for (const data of delta.addedDocuments) { const resource = URI.revive(data.uri); - assert.ok(!this._documents.has(resource), `document '${resource} already exists!'`); + let ref = this._documents.get(resource); - const documentData = new ExtHostDocumentData( - this._extHostRpc.getProxy(MainContext.MainThreadDocuments), - resource, - data.lines, - data.EOL, - data.modeId, - data.versionId, - data.isDirty - ); - this._documents.set(resource, documentData); - addedDocuments.push(documentData); + // double check -> only notebook cell documents should be + // referenced/opened more than once... + if (ref) { + if (resource.scheme !== Schemas.vscodeNotebookCell) { + throw new Error(`document '${resource} already exists!'`); + } + } + if (!ref) { + ref = new Reference(new ExtHostDocumentData( + this._extHostRpc.getProxy(MainContext.MainThreadDocuments), + resource, + data.lines, + data.EOL, + data.versionId, + data.modeId, + data.isDirty, + data.notebook + )); + this._documents.set(resource, ref); + addedDocuments.push(ref.value); + } + + ref.ref(); } } @@ -92,7 +130,7 @@ export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsSha assert.ok(this._documents.has(resource), `document '${resource}' does not exist`); assert.ok(!this._editors.has(data.id), `editor '${data.id}' already exists!`); - const documentData = this._documents.get(resource)!; + const documentData = this._documents.get(resource)!.value; const editor = new ExtHostTextEditor( data.id, this._extHostRpc.getProxy(MainContext.MainThreadTextEditors), @@ -132,11 +170,11 @@ export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsSha } getDocument(uri: URI): ExtHostDocumentData | undefined { - return this._documents.get(uri); + return this._documents.get(uri)?.value; } - allDocuments(): ExtHostDocumentData[] { - return [...this._documents.values()]; + allDocuments(): Iterable { + return Iterable.map(this._documents.values(), ref => ref.value); } getEditor(id: string): ExtHostTextEditor | undefined { diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 34639e18b6f..0bb5188614b 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -5,7 +5,6 @@ import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; -import * as platform from 'vs/base/common/platform'; import { originalFSPath, joinPath } from 'vs/base/common/resources'; import { Barrier, timeout } from 'vs/base/common/async'; import { dispose, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; @@ -17,7 +16,7 @@ import { ExtHostConfiguration, IExtHostConfiguration } from 'vs/workbench/api/co import { ActivatedExtension, EmptyExtension, ExtensionActivationReason, ExtensionActivationTimes, ExtensionActivationTimesBuilder, ExtensionsActivator, IExtensionAPI, IExtensionModule, HostExtension, ExtensionActivationTimesFragment } from 'vs/workbench/api/common/extHostExtensionActivator'; import { ExtHostStorage, IExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; import { ExtHostWorkspace, IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { ExtensionActivationError, checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionActivationError, checkProposedApiEnabled, ActivationKind } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import * as errors from 'vs/base/common/errors'; import type * as vscode from 'vscode'; @@ -25,7 +24,7 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import { Schemas } from 'vs/base/common/network'; import { VSBuffer } from 'vs/base/common/buffer'; import { ExtensionMemento } from 'vs/workbench/api/common/extHostMemento'; -import { RemoteAuthorityResolverError, ExtensionMode } from 'vs/workbench/api/common/extHostTypes'; +import { RemoteAuthorityResolverError, ExtensionMode, ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes'; import { ResolvedAuthority, ResolvedOptions, RemoteAuthorityResolverErrorCode, IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; @@ -71,6 +70,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme readonly _serviceBrand: undefined; + abstract readonly extensionRuntime: ExtensionRuntime; private readonly _onDidChangeRemoteConnectionData = this._register(new Emitter()); public readonly onDidChangeRemoteConnectionData = this._onDidChangeRemoteConnectionData.event; @@ -384,30 +384,18 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme subscriptions: [], get extensionUri() { return extensionDescription.extensionLocation; }, get extensionPath() { return extensionDescription.extensionLocation.fsPath; }, - asAbsolutePath(relativePath: string) { - if (platform.isWeb) { - // web worker - return URI.joinPath(extensionDescription.extensionLocation, relativePath).toString(); - } else { - return path.join(extensionDescription.extensionLocation.fsPath, relativePath); - } - }, + asAbsolutePath(relativePath: string) { return path.join(extensionDescription.extensionLocation.fsPath, relativePath); }, get storagePath() { return that._storagePath.workspaceValue(extensionDescription)?.fsPath; }, get globalStoragePath() { return that._storagePath.globalValue(extensionDescription).fsPath; }, get logPath() { return path.join(that._initData.logsLocation.fsPath, extensionDescription.identifier.value); }, - get logUri() { - checkProposedApiEnabled(extensionDescription); - return URI.joinPath(that._initData.logsLocation, extensionDescription.identifier.value); - }, - get storageUri() { - checkProposedApiEnabled(extensionDescription); - return that._storagePath.workspaceValue(extensionDescription); - }, - get globalStorageUri() { - checkProposedApiEnabled(extensionDescription); - return that._storagePath.globalValue(extensionDescription); - }, + get logUri() { return URI.joinPath(that._initData.logsLocation, extensionDescription.identifier.value); }, + get storageUri() { return that._storagePath.workspaceValue(extensionDescription); }, + get globalStorageUri() { return that._storagePath.globalValue(extensionDescription); }, get extensionMode() { return extensionMode; }, + get extensionRuntime() { + checkProposedApiEnabled(extensionDescription); + return that.extensionRuntime; + }, get environmentVariableCollection() { return that._extHostTerminalService.getEnvironmentVariableCollection(extensionDescription); } }); }); @@ -690,7 +678,11 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return this._startExtensionHost(); } - public $activateByEvent(activationEvent: string): Promise { + public $activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise { + if (activationKind === ActivationKind.Immediate) { + return this._activateByEvent(activationEvent, false); + } + return ( this._readyToRunExtensions.wait() .then(_ => this._activateByEvent(activationEvent, false)) diff --git a/src/vs/workbench/api/common/extHostFileSystemEventService.ts b/src/vs/workbench/api/common/extHostFileSystemEventService.ts index 41606d40393..76a5f2f81e9 100644 --- a/src/vs/workbench/api/common/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/common/extHostFileSystemEventService.ts @@ -8,12 +8,11 @@ import { IRelativePattern, parse } from 'vs/base/common/glob'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import type * as vscode from 'vscode'; -import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, MainThreadTextEditorsShape, IWorkspaceFileEditDto, IWorkspaceTextEditDto, SourceTargetPair } from './extHost.protocol'; +import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, MainThreadTextEditorsShape, SourceTargetPair, IWorkspaceEditDto } from './extHost.protocol'; 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 { flatten } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ILogService } from 'vs/platform/log/common/log'; @@ -217,14 +216,13 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ } if (edits.length > 0) { - // flatten all WorkspaceEdits collected via waitUntil-call - // and apply them in one go. - const allEdits = new Array>(); + // concat all WorkspaceEdits collected via waitUntil-call and apply them in one go. + const dto: IWorkspaceEditDto = { edits: [] }; for (let edit of edits) { let { edits } = typeConverter.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors); - allEdits.push(edits); + dto.edits = dto.edits.concat(edits); } - return this._mainThreadTextEditors.$tryApplyWorkspaceEdit({ edits: flatten(allEdits) }); + return this._mainThreadTextEditors.$tryApplyWorkspaceEdit(dto); } } } diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index cbe85a2675e..c912e48141d 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -324,14 +324,17 @@ class OnTypeRenameAdapter { private readonly _provider: vscode.OnTypeRenameProvider ) { } - provideOnTypeRenameRanges(resource: URI, position: IPosition, token: CancellationToken): Promise { + provideOnTypeRenameRanges(resource: URI, position: IPosition, token: CancellationToken): Promise<{ ranges: IRange[]; wordPattern?: RegExp; } | undefined> { const doc = this._documents.getDocument(resource); const pos = typeConvert.Position.to(position); return asPromise(() => this._provider.provideOnTypeRenameRanges(doc, pos, token)).then(value => { - if (Array.isArray(value)) { - return coalesce(value.map(typeConvert.Range.from)); + if (value && Array.isArray(value.ranges)) { + return { + ranges: coalesce(value.ranges.map(typeConvert.Range.from)), + wordPattern: value.wordPattern + }; } return undefined; }); @@ -1117,7 +1120,7 @@ class ColorProviderAdapter { provideColors(resource: URI, token: CancellationToken): Promise { const doc = this._documents.getDocument(resource); return asPromise(() => this._provider.provideDocumentColors(doc, token)).then(colors => { - if (!Array.isArray(colors)) { + if (!Array.isArray(colors)) { return []; } @@ -1549,15 +1552,24 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF // --- on type rename - registerOnTypeRenameProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.OnTypeRenameProvider, stopPattern?: RegExp): vscode.Disposable { + registerOnTypeRenameProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.OnTypeRenameProvider, wordPattern?: RegExp): vscode.Disposable { const handle = this._addNewAdapter(new OnTypeRenameAdapter(this._documents, provider), extension); - const serializedStopPattern = stopPattern ? ExtHostLanguageFeatures._serializeRegExp(stopPattern) : undefined; - this._proxy.$registerOnTypeRenameProvider(handle, this._transformDocumentSelector(selector), serializedStopPattern); + const serializedWordPattern = wordPattern ? ExtHostLanguageFeatures._serializeRegExp(wordPattern) : undefined; + this._proxy.$registerOnTypeRenameProvider(handle, this._transformDocumentSelector(selector), serializedWordPattern); return this._createDisposable(handle); } - $provideOnTypeRenameRanges(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise { - return this._withAdapter(handle, OnTypeRenameAdapter, adapter => adapter.provideOnTypeRenameRanges(URI.revive(resource), position, token), undefined); + $provideOnTypeRenameRanges(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise<{ ranges: IRange[]; wordPattern?: extHostProtocol.IRegExpDto; } | undefined> { + return this._withAdapter(handle, OnTypeRenameAdapter, async adapter => { + const res = await adapter.provideOnTypeRenameRanges(URI.revive(resource), position, token); + if (res) { + return { + ranges: res.ranges, + wordPattern: res.wordPattern ? ExtHostLanguageFeatures._serializeRegExp(res.wordPattern) : undefined + }; + } + return undefined; + }, undefined); } // --- references diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 9d9c567c468..47714ac961a 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -7,25 +7,25 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance import { readonly } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { hash } from 'vs/base/common/hash'; -import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { joinPath } from 'vs/base/common/resources'; import { ISplice } from 'vs/base/common/sequence'; -import { NotImplementedProxy } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as UUID from 'vs/base/common/uuid'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { CellKind, ExtHostNotebookShape, IMainContext, INotebookDocumentsAndEditorsDelta, INotebookEditorPropertiesChangeData, MainContext, MainThreadDocumentsShape, MainThreadNotebookShape, NotebookCellOutputsSplice } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; -import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { CellKind, ExtHostNotebookShape, ICommandDto, IMainContext, IModelAddedData, INotebookDocumentPropertiesChangeData, INotebookDocumentsAndEditorsDelta, INotebookEditorPropertiesChangeData, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice } from 'vs/workbench/api/common/extHost.protocol'; +import { ILogService } from 'vs/platform/log/common/log'; +import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { ExtHostDocumentsAndEditors, IExtHostModelAddedData } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; -import { CellEditType, CellOutputKind, diff, ICellDeleteEdit, ICellEditOperation, ICellInsertEdit, IMainCellDto, INotebookDisplayOrder, INotebookEditData, INotebookKernelInfoDto2, IOutputRenderRequest, IOutputRenderResponse, IOutputRenderResponseCellInfo, IOutputRenderResponseOutputInfo, IProcessedOutput, IRawOutput, NotebookCellMetadata, NotebookCellsChangedEvent, NotebookCellsChangeType, NotebookCellsSplice2, NotebookDataDto, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { addIdToOutput, CellEditType, CellOutputKind, CellStatusbarAlignment, CellUri, diff, ICellEditOperation, ICellReplaceEdit, IMainCellDto, INotebookCellStatusBarEntry, INotebookDisplayOrder, INotebookEditData, INotebookKernelInfoDto2, IProcessedOutput, NotebookCellMetadata, NotebookCellsChangedEvent, NotebookCellsChangeType, NotebookCellsSplice2, NotebookDataDto, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import * as vscode from 'vscode'; import { Cache } from './cache'; +import { ResourceMap } from 'vs/base/common/map'; interface IObservable { proxy: T; @@ -55,30 +55,22 @@ interface INotebookEventEmitter { emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void; } -const addIdToOutput = (output: IRawOutput, id = UUID.generateUuid()): IProcessedOutput => output.outputKind === CellOutputKind.Rich - ? ({ ...output, outputId: id }) : output; +export class ExtHostCell extends Disposable { -class DettachedCellDocumentData extends ExtHostDocumentData { - - private static readonly _fakeProxy = new class extends NotImplementedProxy('document') { - $trySaveDocument() { - return Promise.reject('Cell-document cannot be saved'); - } - }; - - constructor(cell: IMainCellDto) { - super(DettachedCellDocumentData._fakeProxy, - URI.revive(cell.uri), - cell.source, - cell.eol, - cell.language, - 0, - false - ); + public static asModelAddData(notebook: vscode.NotebookDocument, cell: IMainCellDto): IExtHostModelAddedData { + return { + EOL: cell.eol, + lines: cell.source, + modeId: cell.language, + uri: cell.uri, + isDirty: false, + versionId: 1, + notebook + }; } -} -export class ExtHostCell extends Disposable implements vscode.NotebookCell { + private _onDidDispose = new Emitter(); + readonly onDidDispose: Event = this._onDidDispose.event; private _onDidChangeOutputs = new Emitter[]>(); readonly onDidChangeOutputs: Event[]> = this._onDidChangeOutputs.event; @@ -93,52 +85,65 @@ export class ExtHostCell extends Disposable implements vscode.NotebookCell { readonly uri: URI; readonly cellKind: CellKind; - // todo@jrieken this is a little fish because we have - // vscode.TextDocument for which we never fired an onDidOpen - // event and which doesn't appear in the list of documents. - // this will change once the "real" document comes along. We - // should come up with a better approach here... - readonly defaultDocument: DettachedCellDocumentData; + private _cell: vscode.NotebookCell | undefined; constructor( - private _proxy: MainThreadNotebookShape, - readonly notebook: ExtHostNotebookDocument, - private _extHostDocument: ExtHostDocumentsAndEditors, - cell: IMainCellDto, + private readonly _proxy: MainThreadNotebookShape, + private readonly _notebook: ExtHostNotebookDocument, + private readonly _extHostDocument: ExtHostDocumentsAndEditors, + private readonly _cellData: IMainCellDto, ) { super(); - this.handle = cell.handle; - this.uri = URI.revive(cell.uri); - this.cellKind = cell.cellKind; - this.defaultDocument = new DettachedCellDocumentData(cell); + this.handle = _cellData.handle; + this.uri = URI.revive(_cellData.uri); + this.cellKind = _cellData.cellKind; - this._outputs = cell.outputs; + this._outputs = _cellData.outputs; for (const output of this._outputs) { this._outputMapping.set(output, output.outputId); delete output.outputId; } - const observableMetadata = getObservable(cell.metadata ?? {}); + const observableMetadata = getObservable(_cellData.metadata ?? {}); this._metadata = observableMetadata.proxy; this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { this._updateMetadata(); })); } - get document(): vscode.TextDocument { - return this._extHostDocument.getDocument(this.uri)?.document ?? this.defaultDocument.document; + get cell(): vscode.NotebookCell { + if (!this._cell) { + const that = this; + const document = this._extHostDocument.getDocument(this.uri)!.document; + this._cell = Object.freeze({ + notebook: that._notebook.notebookDocument, + uri: that.uri, + cellKind: this._cellData.cellKind, + document, + language: document.languageId, + get outputs() { return that._outputs; }, + set outputs(value) { that._updateOutputs(value); }, + get metadata() { return that._metadata; }, + set metadata(value) { + that.setMetadata(value); + that._updateMetadata(); + }, + }); + } + return this._cell; } - get language(): string { - return this.document.languageId; + dispose() { + super.dispose(); + this._onDidDispose.fire(); } - get outputs() { - return this._outputs; + setOutputs(newOutputs: vscode.CellOutput[]): void { + this._outputs = newOutputs; } - set outputs(newOutputs: vscode.CellOutput[]) { + private _updateOutputs(newOutputs: vscode.CellOutput[]) { const rawDiffs = diff(this._outputs || [], newOutputs || [], (a) => { return this._outputMapping.has(a); }); @@ -168,15 +173,6 @@ export class ExtHostCell extends Disposable implements vscode.NotebookCell { this._onDidChangeOutputs.fire(transformedDiffs); } - get metadata() { - return this._metadata; - } - - set metadata(newMetadata: vscode.NotebookCellMetadata) { - this.setMetadata(newMetadata); - this._updateMetadata(); - } - setMetadata(newMetadata: vscode.NotebookCellMetadata): void { // Don't apply metadata defaults here, 'undefined' means 'inherit from document metadata' this._metadataChangeListener.dispose(); @@ -188,11 +184,26 @@ export class ExtHostCell extends Disposable implements vscode.NotebookCell { } private _updateMetadata(): Promise { - return this._proxy.$updateNotebookCellMetadata(this.notebook.viewType, this.notebook.uri, this.handle, this._metadata); + return this._proxy.$updateNotebookCellMetadata(this._notebook.notebookDocument.viewType, this._notebook.uri, this.handle, this._metadata); } } -export class ExtHostNotebookDocument extends Disposable implements vscode.NotebookDocument { +class RawContentChangeEvent { + + constructor(readonly start: number, readonly deletedCount: number, readonly deletedItems: ExtHostCell[], readonly items: ExtHostCell[]) { } + + static asApiEvent(event: RawContentChangeEvent): vscode.NotebookCellsChangeData { + return Object.freeze({ + start: event.start, + deletedCount: event.deletedCount, + deletedItems: event.deletedItems.map(data => data.cell), + items: event.items.map(data => data.cell) + }); + } +} + +export class ExtHostNotebookDocument extends Disposable { + private static _handlePool: number = 0; readonly handle = ExtHostNotebookDocument._handlePool++; @@ -200,33 +211,45 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo private _cellDisposableMapping = new Map(); - get cells() { - return this._cells; - } - - private _languages: string[] = []; - - get languages() { - return this._languages = []; - } - - set languages(newLanguages: string[]) { - this._languages = newLanguages; - this._proxy.$updateNotebookLanguages(this.viewType, this.uri, this._languages); - } - - get isUntitled() { - return this.uri.scheme === Schemas.untitled; - } + private _notebook: vscode.NotebookDocument | undefined; private _metadata: Required = notebookDocumentMetadataDefaults; private _metadataChangeListener: IDisposable; + private _displayOrder: string[] = []; + private _versionId = 0; + private _isDirty: boolean = false; + private _backupCounter = 1; + private _backup?: vscode.NotebookDocumentBackup; + private _disposed = false; + private _languages: string[] = []; - get metadata() { - return this._metadata; + private readonly _edits = new Cache('notebook documents'); + + constructor( + private readonly _proxy: MainThreadNotebookShape, + private readonly _documentsAndEditors: ExtHostDocumentsAndEditors, + private readonly _emitter: INotebookEventEmitter, + private readonly _viewType: string, + public readonly uri: URI, + public readonly renderingHandler: ExtHostNotebookOutputRenderingHandler, + private readonly _storagePath: URI | undefined + ) { + super(); + + const observableMetadata = getObservable(notebookDocumentMetadataDefaults); + this._metadata = observableMetadata.proxy; + this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { + this._tryUpdateMetadata(); + })); } - set metadata(newMetadata: Required) { + dispose() { + this._disposed = true; + super.dispose(); + dispose(this._cellDisposableMapping.values()); + } + + private _updateMetadata(newMetadata: Required) { this._metadataChangeListener.dispose(); newMetadata = { ...notebookDocumentMetadataDefaults, @@ -239,34 +262,220 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo const observableMetadata = getObservable(newMetadata); this._metadata = observableMetadata.proxy; this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { - this.updateMetadata(); + this._tryUpdateMetadata(); })); - this.updateMetadata(); + this._tryUpdateMetadata(); } - private _displayOrder: string[] = []; - - get displayOrder() { - return this._displayOrder; + private _tryUpdateMetadata() { + this._proxy.$updateNotebookMetadata(this._viewType, this.uri, this._metadata); + } + get notebookDocument(): vscode.NotebookDocument { + if (!this._notebook) { + const that = this; + this._notebook = Object.freeze({ + get uri() { return that.uri; }, + get version() { return that._versionId; }, + get fileName() { return that.uri.fsPath; }, + get viewType() { return that._viewType; }, + get isDirty() { return that._isDirty; }, + get isUntitled() { return that.uri.scheme === Schemas.untitled; }, + get cells(): ReadonlyArray { return that._cells.map(cell => cell.cell); }, + get languages() { return that._languages; }, + set languages(value: string[]) { that._trySetLanguages(value); }, + get displayOrder() { return that._displayOrder; }, + set displayOrder(value: string[]) { that._displayOrder = value; }, + get metadata() { return that._metadata; }, + set metadata(value: Required) { that._updateMetadata(value); }, + }); + } + return this._notebook; } - set displayOrder(newOrder: string[]) { - this._displayOrder = newOrder; + private _trySetLanguages(newLanguages: string[]) { + this._languages = newLanguages; + this._proxy.$updateNotebookLanguages(this._viewType, this.uri, this._languages); } - private _versionId = 0; - - get versionId() { - return this._versionId; + getNewBackupUri(): URI { + if (!this._storagePath) { + throw new Error('Backup requires a valid storage path'); + } + const fileName = hashPath(this.uri) + (this._backupCounter++); + return joinPath(this._storagePath, fileName); } - private _backupCounter = 1; + updateBackup(backup: vscode.NotebookDocumentBackup): void { + this._backup?.delete(); + this._backup = backup; + } - private _backup?: vscode.NotebookDocumentBackup; + disposeBackup(): void { + this._backup?.delete(); + this._backup = undefined; + } + acceptModelChanged(event: NotebookCellsChangedEvent, isDirty: boolean): void { + this._versionId = event.versionId; + this._isDirty = isDirty; + if (event.kind === NotebookCellsChangeType.Initialize) { + this._spliceNotebookCells(event.changes, true); + } if (event.kind === NotebookCellsChangeType.ModelChange) { + this._spliceNotebookCells(event.changes, false); + } else if (event.kind === NotebookCellsChangeType.Move) { + this._moveCell(event.index, event.newIdx); + } else if (event.kind === NotebookCellsChangeType.Output) { + this._setCellOutputs(event.index, event.outputs); + } else if (event.kind === NotebookCellsChangeType.CellClearOutput) { + this._clearCellOutputs(event.index); + } else if (event.kind === NotebookCellsChangeType.CellsClearOutput) { + this._clearAllCellOutputs(); + } else if (event.kind === NotebookCellsChangeType.ChangeLanguage) { + this._changeCellLanguage(event.index, event.language); + } else if (event.kind === NotebookCellsChangeType.ChangeMetadata) { + this._changeCellMetadata(event.index, event.metadata); + } + } - private readonly _edits = new Cache('notebook documents'); + private _spliceNotebookCells(splices: NotebookCellsSplice2[], initialization: boolean): void { + if (this._disposed) { + return; + } + + const contentChangeEvents: RawContentChangeEvent[] = []; + const addedCellDocuments: IExtHostModelAddedData[] = []; + const removedCellDocuments: URI[] = []; + + splices.reverse().forEach(splice => { + const cellDtos = splice[2]; + const newCells = cellDtos.map(cell => { + + const extCell = new ExtHostCell(this._proxy, this, this._documentsAndEditors, cell); + + if (!initialization) { + addedCellDocuments.push(ExtHostCell.asModelAddData(this.notebookDocument, cell)); + } + + if (!this._cellDisposableMapping.has(extCell.handle)) { + const store = new DisposableStore(); + store.add(extCell); + this._cellDisposableMapping.set(extCell.handle, store); + } + + const store = this._cellDisposableMapping.get(extCell.handle)!; + + store.add(extCell.onDidChangeOutputs((diffs) => { + this.eventuallyUpdateCellOutputs(extCell, diffs); + })); + + return extCell; + }); + + for (let j = splice[0]; j < splice[0] + splice[1]; j++) { + this._cellDisposableMapping.get(this._cells[j].handle)?.dispose(); + this._cellDisposableMapping.delete(this._cells[j].handle); + } + + const deletedItems = this._cells.splice(splice[0], splice[1], ...newCells); + for (let cell of deletedItems) { + removedCellDocuments.push(cell.uri); + } + + contentChangeEvents.push(new RawContentChangeEvent(splice[0], splice[1], deletedItems, newCells)); + }); + + this._documentsAndEditors.acceptDocumentsAndEditorsDelta({ + addedDocuments: addedCellDocuments, + removedDocuments: removedCellDocuments + }); + + if (!initialization) { + this._emitter.emitModelChange({ + document: this.notebookDocument, + changes: contentChangeEvents.map(RawContentChangeEvent.asApiEvent) + }); + } + } + + private _moveCell(index: number, newIdx: number): void { + const cells = this._cells.splice(index, 1); + this._cells.splice(newIdx, 0, ...cells); + const changes: vscode.NotebookCellsChangeData[] = [{ + start: index, + deletedCount: 1, + deletedItems: cells.map(data => data.cell), + items: [] + }, { + start: newIdx, + deletedCount: 0, + deletedItems: [], + items: cells.map(data => data.cell) + }]; + this._emitter.emitModelChange({ + document: this.notebookDocument, + changes + }); + } + + private _setCellOutputs(index: number, outputs: IProcessedOutput[]): void { + const cell = this._cells[index]; + cell.setOutputs(outputs); + this._emitter.emitCellOutputsChange({ document: this.notebookDocument, cells: [cell.cell] }); + } + + private _clearCellOutputs(index: number): void { + const cell = this._cells[index].cell; + cell.outputs = []; + const event: vscode.NotebookCellOutputsChangeEvent = { document: this.notebookDocument, cells: [cell] }; + this._emitter.emitCellOutputsChange(event); + } + + private _clearAllCellOutputs(): void { + const modifedCells: vscode.NotebookCell[] = []; + this._cells.forEach(({ cell }) => { + if (cell.outputs.length !== 0) { + cell.outputs = []; + modifedCells.push(cell); + } + }); + const event: vscode.NotebookCellOutputsChangeEvent = { document: this.notebookDocument, cells: modifedCells }; + this._emitter.emitCellOutputsChange(event); + } + + private _changeCellLanguage(index: number, language: string): void { + const cell = this._cells[index]; + const event: vscode.NotebookCellLanguageChangeEvent = { document: this.notebookDocument, cell: cell.cell, language }; + this._emitter.emitCellLanguageChange(event); + } + + private _changeCellMetadata(index: number, newMetadata: NotebookCellMetadata | undefined): void { + const cell = this._cells[index]; + cell.setMetadata(newMetadata || {}); + const event: vscode.NotebookCellMetadataChangeEvent = { document: this.notebookDocument, cell: cell.cell }; + this._emitter.emitCellMetadataChange(event); + } + + async eventuallyUpdateCellOutputs(cell: ExtHostCell, diffs: ISplice[]) { + const outputDtos: NotebookCellOutputsSplice[] = diffs.map(diff => { + const outputs = diff.toInsert; + return [diff.start, diff.deleteCount, outputs]; + }); + + if (!outputDtos.length) { + return; + } + + await this._proxy.$spliceNotebookCellOutputs(this._viewType, this.uri, cell.handle, outputDtos); + this._emitter.emitCellOutputsChange({ + document: this.notebookDocument, + cells: [cell.cell] + }); + } + + getCell(cellHandle: number): ExtHostCell | undefined { + return this._cells.find(cell => cell.handle === cellHandle); + } addEdit(item: vscode.NotebookDocumentEditEvent): number { @@ -301,216 +510,16 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo this._edits.delete(id); } } - - private _disposed = false; - - constructor( - private readonly _proxy: MainThreadNotebookShape, - private _documentsAndEditors: ExtHostDocumentsAndEditors, - private _emitter: INotebookEventEmitter, - public viewType: string, - public uri: URI, - public renderingHandler: ExtHostNotebookOutputRenderingHandler, - private readonly _storagePath: URI | undefined - ) { - super(); - - const observableMetadata = getObservable(notebookDocumentMetadataDefaults); - this._metadata = observableMetadata.proxy; - this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { - this.updateMetadata(); - })); - } - - private updateMetadata() { - this._proxy.$updateNotebookMetadata(this.viewType, this.uri, this._metadata); - } - - getNewBackupUri(): URI { - if (!this._storagePath) { - throw new Error('Backup requires a valid storage path'); - } - const fileName = hashPath(this.uri) + (this._backupCounter++); - return joinPath(this._storagePath, fileName); - } - - updateBackup(backup: vscode.NotebookDocumentBackup): void { - this._backup?.delete(); - this._backup = backup; - } - - disposeBackup(): void { - this._backup?.delete(); - this._backup = undefined; - } - - dispose() { - this._disposed = true; - super.dispose(); - this._cellDisposableMapping.forEach(cell => cell.dispose()); - } - - get fileName() { return this.uri.fsPath; } - - get isDirty() { return false; } - - acceptModelChanged(event: NotebookCellsChangedEvent): void { - this._versionId = event.versionId; - if (event.kind === NotebookCellsChangeType.Initialize) { - this.$spliceNotebookCells(event.changes, true); - } if (event.kind === NotebookCellsChangeType.ModelChange) { - this.$spliceNotebookCells(event.changes, false); - } else if (event.kind === NotebookCellsChangeType.Move) { - this.$moveCell(event.index, event.newIdx); - } else if (event.kind === NotebookCellsChangeType.CellClearOutput) { - this.$clearCellOutputs(event.index); - } else if (event.kind === NotebookCellsChangeType.CellsClearOutput) { - this.$clearAllCellOutputs(); - } else if (event.kind === NotebookCellsChangeType.ChangeLanguage) { - this.$changeCellLanguage(event.index, event.language); - } else if (event.kind === NotebookCellsChangeType.ChangeMetadata) { - this.$changeCellMetadata(event.index, event.metadata); - } - } - - private $spliceNotebookCells(splices: NotebookCellsSplice2[], initialization: boolean): void { - if (this._disposed) { - return; - } - - const contentChangeEvents: vscode.NotebookCellsChangeData[] = []; - - splices.reverse().forEach(splice => { - const cellDtos = splice[2]; - const newCells = cellDtos.map(cell => { - - const extCell = new ExtHostCell(this._proxy, this, this._documentsAndEditors, cell); - - if (!this._cellDisposableMapping.has(extCell.handle)) { - this._cellDisposableMapping.set(extCell.handle, new DisposableStore()); - } - - const store = this._cellDisposableMapping.get(extCell.handle)!; - - store.add(extCell.onDidChangeOutputs((diffs) => { - this.eventuallyUpdateCellOutputs(extCell, diffs); - })); - - return extCell; - }); - - for (let j = splice[0]; j < splice[0] + splice[1]; j++) { - this._cellDisposableMapping.get(this.cells[j].handle)?.dispose(); - this._cellDisposableMapping.delete(this.cells[j].handle); - - } - - const deletedItems = this.cells.splice(splice[0], splice[1], ...newCells); - - const event: vscode.NotebookCellsChangeData = { - start: splice[0], - deletedCount: splice[1], - deletedItems, - items: newCells - }; - - contentChangeEvents.push(event); - }); - - if (!initialization) { - this._emitter.emitModelChange({ - document: this, - changes: contentChangeEvents - }); - } - } - - private $moveCell(index: number, newIdx: number): void { - const cells = this.cells.splice(index, 1); - this.cells.splice(newIdx, 0, ...cells); - const changes: vscode.NotebookCellsChangeData[] = [{ - start: index, - deletedCount: 1, - deletedItems: cells, - items: [] - }, { - start: newIdx, - deletedCount: 0, - deletedItems: [], - items: cells - }]; - this._emitter.emitModelChange({ - document: this, - changes - }); - } - - private $clearCellOutputs(index: number): void { - const cell = this.cells[index]; - cell.outputs = []; - const event: vscode.NotebookCellOutputsChangeEvent = { document: this, cells: [cell] }; - this._emitter.emitCellOutputsChange(event); - } - - private $clearAllCellOutputs(): void { - const modifedCells: vscode.NotebookCell[] = []; - this.cells.forEach(cell => { - if (cell.outputs.length !== 0) { - cell.outputs = []; - modifedCells.push(cell); - } - }); - const event: vscode.NotebookCellOutputsChangeEvent = { document: this, cells: modifedCells }; - this._emitter.emitCellOutputsChange(event); - } - - private $changeCellLanguage(index: number, language: string): void { - const cell = this.cells[index]; - cell.defaultDocument._acceptLanguageId(language); - const event: vscode.NotebookCellLanguageChangeEvent = { document: this, cell, language }; - this._emitter.emitCellLanguageChange(event); - } - - private $changeCellMetadata(index: number, newMetadata: NotebookCellMetadata): void { - const cell = this.cells[index]; - cell.setMetadata(newMetadata); - const event: vscode.NotebookCellMetadataChangeEvent = { document: this, cell }; - this._emitter.emitCellMetadataChange(event); - } - - async eventuallyUpdateCellOutputs(cell: ExtHostCell, diffs: ISplice[]) { - const renderers = new Set(); - const outputDtos: NotebookCellOutputsSplice[] = diffs.map(diff => { - const outputs = diff.toInsert; - return [diff.start, diff.deleteCount, outputs]; - }); - - await this._proxy.$spliceNotebookCellOutputs(this.viewType, this.uri, cell.handle, outputDtos, Array.from(renderers)); - this._emitter.emitCellOutputsChange({ - document: this, - cells: [cell] - }); - } - - getCell(cellHandle: number) { - return this.cells.find(cell => cell.handle === cellHandle); - } - - getCell2(cellUri: UriComponents) { - return this.cells.find(cell => cell.uri.fragment === cellUri.fragment); - } } export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorCellEdit { - private _finalized: boolean = false; - private readonly _documentVersionId: number; - private _collectedEdits: ICellEditOperation[] = []; - private _renderers = new Set(); - constructor( - readonly editor: ExtHostNotebookEditor - ) { - this._documentVersionId = editor.document.versionId; + private readonly _documentVersionId: number; + private readonly _collectedEdits: ICellEditOperation[] = []; + private _finalized: boolean = false; + + constructor(documentVersionId: number) { + this._documentVersionId = documentVersionId; } finalize(): INotebookEditData { @@ -518,7 +527,6 @@ export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorCellE return { documentVersionId: this._documentVersionId, edits: this._collectedEdits, - renderers: Array.from(this._renderers) }; } @@ -528,33 +536,54 @@ export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorCellE } } - insert(index: number, content: string | string[], language: string, type: CellKind, outputs: vscode.CellOutput[], metadata: vscode.NotebookCellMetadata | undefined): void { + replaceMetadata(index: number, metadata: vscode.NotebookCellMetadata): void { + this._throwIfFinalized(); + this._collectedEdits.push({ + editType: CellEditType.Metadata, + index, + metadata + }); + } + + replaceOutput(index: number, outputs: vscode.CellOutput[]): void { + this._throwIfFinalized(); + this._collectedEdits.push({ + editType: CellEditType.Output, + index, + outputs: outputs.map(output => addIdToOutput(output)) + }); + } + + replaceCells(from: number, to: number, cells: vscode.NotebookCellData[]): void { this._throwIfFinalized(); - const sourceArr = Array.isArray(content) ? content : content.split(/\r|\n|\r\n/g); - const cell = { - source: sourceArr, - language, - cellKind: type, - outputs: outputs.map(o => addIdToOutput(o)), - metadata, - }; - this._collectedEdits.push({ - editType: CellEditType.Insert, - index, - cells: [cell] + editType: CellEditType.Replace, + index: from, + count: to - from, + cells: cells.map(data => { + return { + ...data, + outputs: data.outputs.map(output => addIdToOutput(output)), + }; + }) }); } + insert(index: number, content: string | string[], language: string, type: CellKind, outputs: vscode.CellOutput[], metadata: vscode.NotebookCellMetadata | undefined): void { + this._throwIfFinalized(); + this.replaceCells(index, index, [{ + language, + outputs, + metadata, + cellKind: type, + source: Array.isArray(content) ? content.join('\n') : content, + }]); + } + delete(index: number): void { this._throwIfFinalized(); - - this._collectedEdits.push({ - editType: CellEditType.Delete, - index, - count: 1 - }); + this.replaceCells(index, 1, []); } } @@ -606,7 +635,21 @@ class ExtHostWebviewCommWrapper extends Disposable { export class ExtHostNotebookEditor extends Disposable implements vscode.NotebookEditor { private _viewColumn: vscode.ViewColumn | undefined; - selection?: ExtHostCell = undefined; + selection?: vscode.NotebookCell; + + private _visibleRanges: vscode.NotebookCellRange[] = []; + + get visibleRanges() { + return this._visibleRanges; + } + + set visibleRanges(_range: vscode.NotebookCellRange[]) { + throw readonly('visibleRanges'); + } + + _acceptVisibleRanges(value: vscode.NotebookCellRange[]): void { + this._visibleRanges = value; + } private _active: boolean = false; get active(): boolean { @@ -655,7 +698,7 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook public uri: URI, private _proxy: MainThreadNotebookShape, private _webComm: vscode.NotebookCommunication, - public document: ExtHostNotebookDocument, + public readonly notebookData: ExtHostNotebookDocument, ) { super(); this._register(this._webComm.onDidReceiveMessage(e => { @@ -663,14 +706,17 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook })); } - edit(callback: (editBuilder: NotebookEditorCellEditBuilder) => void): Thenable { - const edit = new NotebookEditorCellEditBuilder(this); - callback(edit); - return this._applyEdit(edit); + get document(): vscode.NotebookDocument { + return this.notebookData.notebookDocument; } - private _applyEdit(editBuilder: NotebookEditorCellEditBuilder): Promise { - const editData = editBuilder.finalize(); + edit(callback: (editBuilder: NotebookEditorCellEditBuilder) => void): Thenable { + const edit = new NotebookEditorCellEditBuilder(this.document.version); + callback(edit); + return this._applyEdit(edit.finalize()); + } + + private _applyEdit(editData: INotebookEditData): Promise { // return when there is nothing to do if (editData.edits.length === 0) { @@ -690,16 +736,10 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook const prevIndex = compressedEditsIndex; const prev = compressedEdits[prevIndex]; - if (prev.editType === CellEditType.Insert && editData.edits[i].editType === CellEditType.Insert) { + if (prev.editType === CellEditType.Replace && editData.edits[i].editType === CellEditType.Replace) { if (prev.index === editData.edits[i].index) { - prev.cells.push(...(editData.edits[i] as ICellInsertEdit).cells); - continue; - } - } - - if (prev.editType === CellEditType.Delete && editData.edits[i].editType === CellEditType.Delete) { - if (prev.index === editData.edits[i].index) { - prev.count += (editData.edits[i] as ICellDeleteEdit).count; + prev.cells.push(...(editData.edits[i] as ICellReplaceEdit).cells); + prev.count += (editData.edits[i] as ICellReplaceEdit).count; continue; } } @@ -708,7 +748,11 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook compressedEditsIndex++; } - return this._proxy.$tryApplyEdits(this.viewType, this.uri, editData.documentVersionId, compressedEdits, editData.renderers); + return this._proxy.$tryApplyEdits(this.viewType, this.uri, editData.documentVersionId, compressedEdits); + } + + revealRange(range: vscode.NotebookCellRange, revealType?: extHostTypes.NotebookEditorRevealType) { + this._proxy.$tryRevealRange(this.id, range, revealType || extHostTypes.NotebookEditorRevealType.Default); } get viewColumn(): vscode.ViewColumn | undefined { @@ -735,44 +779,8 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook } } -export class ExtHostNotebookOutputRenderer { - private static _handlePool: number = 0; - private resolvedComms = new WeakSet(); - readonly handle = ExtHostNotebookOutputRenderer._handlePool++; - - constructor( - public type: string, - public filter: vscode.NotebookOutputSelector, - public renderer: vscode.NotebookOutputRenderer - ) { - - } - - matches(mimeType: string): boolean { - if (this.filter.mimeTypes) { - if (this.filter.mimeTypes.indexOf(mimeType) >= 0) { - return true; - } - } - return false; - } - - resolveNotebook(document: ExtHostNotebookDocument, comm: ExtHostWebviewCommWrapper) { - if (!this.resolvedComms.has(comm) && this.renderer.resolveNotebook) { - this.renderer.resolveNotebook(document, comm.getRendererComm(this.type)); - this.resolvedComms.add(comm); - } - } - - render(document: ExtHostNotebookDocument, output: vscode.CellDisplayOutput, outputId: string, mimeType: string): string { - const html = this.renderer.render(document, { output, outputId, mimeType }); - - return html; - } -} export interface ExtHostNotebookOutputRenderingHandler { outputDisplayOrder: INotebookDisplayOrder | undefined; - findBestMatchedRenderer(mimeType: string): ExtHostNotebookOutputRenderer[]; } export class ExtHostNotebookKernelProviderAdapter extends Disposable { @@ -787,14 +795,15 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { super(); if (this._provider.onDidChangeKernels) { - this._register(this._provider.onDidChangeKernels(() => { - this._proxy.$onNotebookKernelChange(this._handle); + this._register(this._provider.onDidChangeKernels((e: vscode.NotebookDocument | undefined) => { + const uri = e?.uri; + this._proxy.$onNotebookKernelChange(this._handle, uri); })); } } async provideKernels(document: ExtHostNotebookDocument, token: vscode.CancellationToken): Promise { - const data = await this._provider.provideKernels(document, token) || []; + const data = await this._provider.provideKernels(document.notebookDocument, token) || []; const newMap = new Map(); let kernel_unique_pool = 0; @@ -820,6 +829,7 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { extension: this._extension.identifier, extensionLocation: this._extension.extensionLocation, description: kernel.description, + detail: kernel.detail, isPreferred: kernel.isPreferred, preloads: kernel.preloads }; @@ -843,7 +853,7 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { const kernel = this._idToKernel.get(kernelId); if (kernel && this._provider.resolveKernel) { - return this._provider.resolveKernel(kernel, document, webview, token); + return this._provider.resolveKernel(kernel, document.notebookDocument, webview, token); } } @@ -855,9 +865,9 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { } if (cell) { - return withToken(token => (kernel.executeCell as any)(document, cell, token)); + return withToken(token => (kernel.executeCell as any)(document.notebookDocument, cell.cell, token)); } else { - return withToken(token => (kernel.executeAllCells as any)(document, token)); + return withToken(token => (kernel.executeAllCells as any)(document.notebookDocument, token)); } } @@ -869,9 +879,9 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { } if (cell) { - return kernel.cancelCellExecution(document, cell); + return kernel.cancelCellExecution(document.notebookDocument, cell.cell); } else { - return kernel.cancelAllCellsExecution(document); + return kernel.cancelAllCellsExecution(document.notebookDocument); } } } @@ -893,12 +903,15 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private readonly _notebookContentProviders = new Map(); private readonly _notebookKernels = new Map(); private readonly _notebookKernelProviders = new Map(); - private readonly _documents = new Map(); - private readonly _unInitializedDocuments = new Map(); + private readonly _documents = new ResourceMap(); + private readonly _unInitializedDocuments = new ResourceMap(); private readonly _editors = new Map(); private readonly _webviewComm = new Map(); - private readonly _notebookOutputRenderers = new Map(); - private readonly _renderersUsedInNotebooks = new WeakMap>(); + private readonly _commandsConverter: CommandsConverter; + private readonly _onDidChangeNotebookEditorSelection = new Emitter(); + readonly onDidChangeNotebookEditorSelection = this._onDidChangeNotebookEditorSelection.event; + private readonly _onDidChangeNotebookEditorVisibleRanges = new Emitter(); + readonly onDidChangeNotebookEditorVisibleRanges = this._onDidChangeNotebookEditorVisibleRanges.event; private readonly _onDidChangeNotebookCells = new Emitter(); readonly onDidChangeNotebookCells = this._onDidChangeNotebookCells.event; private readonly _onDidChangeCellOutputs = new Emitter(); @@ -922,10 +935,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return this._activeNotebookEditor; } - get notebookDocuments() { - return [...this._documents.values()]; - } - private _onDidOpenNotebookDocument = new Emitter(); onDidOpenNotebookDocument: Event = this._onDidOpenNotebookDocument.event; private _onDidCloseNotebookDocument = new Emitter(); @@ -933,7 +942,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private _onDidSaveNotebookDocument = new Emitter(); onDidSaveNotebookDocument: Event = this._onDidCloseNotebookDocument.event; visibleNotebookEditors: ExtHostNotebookEditor[] = []; - private _onDidChangeActiveNotebookKernel = new Emitter<{ document: ExtHostNotebookDocument, kernel: vscode.NotebookKernel | undefined; }>(); + private _onDidChangeActiveNotebookKernel = new Emitter<{ document: vscode.NotebookDocument, kernel: vscode.NotebookKernel | undefined; }>(); onDidChangeActiveNotebookKernel = this._onDidChangeActiveNotebookKernel.event; private _onDidChangeVisibleNotebookEditors = new Emitter(); onDidChangeVisibleNotebookEditors = this._onDidChangeVisibleNotebookEditors.event; @@ -943,21 +952,24 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN commands: ExtHostCommands, private _documentsAndEditors: ExtHostDocumentsAndEditors, private readonly _webviewInitData: WebviewInitData, - private readonly _extensionStoragePaths?: IExtensionStoragePaths, + private readonly logService: ILogService, + private readonly _extensionStoragePaths: IExtensionStoragePaths, ) { this._proxy = mainContext.getProxy(MainContext.MainThreadNotebook); + this._commandsConverter = commands.converter; commands.registerArgumentProcessor({ - processArgument: arg => { + // Serialized INotebookCellActionContext + processArgument: (arg) => { if (arg && arg.$mid === 12) { const documentHandle = arg.notebookEditor?.notebookHandle; const cellHandle = arg.cell.handle; for (const value of this._editors) { - if (value[1].editor.document.handle === documentHandle) { - const cell = value[1].editor.document.getCell(cellHandle); + if (value[1].editor.notebookData.handle === documentHandle) { + const cell = value[1].editor.notebookData.getCell(cellHandle); if (cell) { - return cell; + return cell.cell; } } } @@ -967,127 +979,33 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN }); } - registerNotebookOutputRenderer( - type: string, - extension: IExtensionDescription, - filter: vscode.NotebookOutputSelector, - renderer: vscode.NotebookOutputRenderer - ): vscode.Disposable { - if (this._notebookKernels.has(type)) { - throw new Error(`Notebook renderer for '${type}' already registered`); - } - - const extHostRenderer = new ExtHostNotebookOutputRenderer(type, filter, renderer); - this._notebookOutputRenderers.set(extHostRenderer.type, extHostRenderer); - this._proxy.$registerNotebookRenderer({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, type, filter, renderer.preloads || []); - return new extHostTypes.Disposable(() => { - this._notebookOutputRenderers.delete(extHostRenderer.type); - this._proxy.$unregisterNotebookRenderer(extHostRenderer.type); - }); + get notebookDocuments() { + return [...this._documents.values()]; } - async $renderOutputs(uriComponents: UriComponents, id: string, request: IOutputRenderRequest): Promise | undefined> { - if (!this._notebookOutputRenderers.has(id)) { - throw new Error(`Notebook renderer for '${id}' is not registered`); - } - - const document = this._documents.get(URI.revive(uriComponents).toString()); - - if (!document) { - return; - } - - const renderer = this._notebookOutputRenderers.get(id)!; - this.provideCommToNotebookRenderers(document, renderer); - - const cellsResponse: IOutputRenderResponseCellInfo[] = request.items.map(cellInfo => { - const cell = document.getCell2(cellInfo.key)!; - const outputResponse: IOutputRenderResponseOutputInfo[] = cellInfo.outputs.map(output => { - return { - index: output.index, - outputId: output.outputId, - mimeType: output.mimeType, - handlerId: id, - transformedOutput: renderer.render(document, cell.outputs[output.index] as vscode.CellDisplayOutput, output.outputId, output.mimeType) - }; - }); - - return { - key: cellInfo.key, - outputs: outputResponse - }; - }); - - return { items: cellsResponse }; - } - - /** - * The request carry the raw data for outputs so we don't look up in the existing document - */ - async $renderOutputs2(uriComponents: UriComponents, id: string, request: IOutputRenderRequest): Promise | undefined> { - if (!this._notebookOutputRenderers.has(id)) { - throw new Error(`Notebook renderer for '${id}' is not registered`); - } - - const document = this._documents.get(URI.revive(uriComponents).toString()); - - if (!document) { - return; - } - - const renderer = this._notebookOutputRenderers.get(id)!; - this.provideCommToNotebookRenderers(document, renderer); - - const cellsResponse: IOutputRenderResponseCellInfo[] = request.items.map(cellInfo => { - const outputResponse: IOutputRenderResponseOutputInfo[] = cellInfo.outputs.map(output => { - return { - index: output.index, - outputId: output.outputId, - mimeType: output.mimeType, - handlerId: id, - transformedOutput: renderer.render(document, output.output! as vscode.CellDisplayOutput, output.outputId, output.mimeType) - }; - }); - - return { - key: cellInfo.key, - outputs: outputResponse - }; - }); - - return { items: cellsResponse }; - } - - findBestMatchedRenderer(mimeType: string): ExtHostNotebookOutputRenderer[] { - const matches: ExtHostNotebookOutputRenderer[] = []; - for (const renderer of this._notebookOutputRenderers) { - if (renderer[1].matches(mimeType)) { - matches.push(renderer[1]); - } - } - - return matches; + lookupNotebookDocument(uri: URI): ExtHostNotebookDocument | undefined { + return this._documents.get(uri); } registerNotebookContentProvider( extension: IExtensionDescription, viewType: string, provider: vscode.NotebookContentProvider, + options?: { + transientOutputs: boolean; + transientMetadata: { [K in keyof NotebookCellMetadata]?: boolean }; + } ): vscode.Disposable { if (this._notebookContentProviders.has(viewType)) { throw new Error(`Notebook provider for '${viewType}' already registered`); } - // if ((provider).executeCell) { - // throw new Error('NotebookContentKernel.executeCell is removed, please use vscode.notebook.registerNotebookKernel instead.'); - // } - this._notebookContentProviders.set(viewType, { extension, provider }); const listener = provider.onDidChangeNotebook ? provider.onDidChangeNotebook(e => { - const document = this._documents.get(URI.revive(e.document.uri).toString()); + const document = this._documents.get(URI.revive(e.document.uri)); if (!document) { throw new Error(`Notebook document ${e.document.uri.toString()} not found`); @@ -1104,7 +1022,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const supportBackup = !!provider.backupNotebook; - this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, viewType, supportBackup, provider.kernel ? { id: viewType, label: provider.kernel.label, extensionLocation: extension.extensionLocation, preloads: provider.kernel.preloads } : undefined); + this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, viewType, supportBackup, { transientOutputs: options?.transientOutputs || false, transientMetadata: options?.transientMetadata || {} }); return new extHostTypes.Disposable(() => { listener.dispose(); @@ -1119,8 +1037,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._notebookKernelProviders.set(handle, adapter); this._proxy.$registerNotebookKernelProvider({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, handle, { viewType: selector.viewType, - filenamePattern: selector.filenamePattern ? typeConverters.GlobPattern.from(selector.filenamePattern) : undefined, - excludeFileNamePattern: selector.excludeFileNamePattern ? typeConverters.GlobPattern.from(selector.excludeFileNamePattern) : undefined, + filenamePattern: typeConverters.NotebookExclusiveDocumentPattern.from(selector.filenamePattern) }); return new extHostTypes.Disposable(() => { @@ -1131,7 +1048,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } private _withAdapter(handle: number, uri: UriComponents, callback: (adapter: ExtHostNotebookKernelProviderAdapter, document: ExtHostNotebookDocument) => Promise) { - const document = this._documents.get(URI.revive(uri).toString()); + const document = this._documents.get(URI.revive(uri)); if (!document) { return []; @@ -1162,75 +1079,55 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN }); } - registerNotebookKernel(extension: IExtensionDescription, id: string, selectors: vscode.GlobPattern[], kernel: vscode.NotebookKernel): vscode.Disposable { - if (this._notebookKernels.has(id)) { - throw new Error(`Notebook kernel for '${id}' already registered`); - } - - this._notebookKernels.set(id, { kernel, extension }); - const transformedSelectors = selectors.map(selector => typeConverters.GlobPattern.from(selector)); - - this._proxy.$registerNotebookKernel({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, id, kernel.label, transformedSelectors, kernel.preloads || []); - return new extHostTypes.Disposable(() => { - this._notebookKernels.delete(id); - this._proxy.$unregisterNotebookKernel(id); - }); - } - async $resolveNotebookData(viewType: string, uri: UriComponents, backupId?: string): Promise { const provider = this._notebookContentProviders.get(viewType); const revivedUri = URI.revive(uri); - - if (provider) { - let storageRoot: URI | undefined; - if (this._extensionStoragePaths) { - storageRoot = this._extensionStoragePaths.workspaceValue(provider.extension) ?? this._extensionStoragePaths.globalValue(provider.extension); - } - - let document = this._documents.get(URI.revive(uri).toString()); - - if (!document) { - const that = this; - document = this._unInitializedDocuments.get(revivedUri.toString()) ?? new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, { - emitModelChange(event: vscode.NotebookCellsChangeEvent): void { - that._onDidChangeNotebookCells.fire(event); - }, - emitCellOutputsChange(event: vscode.NotebookCellOutputsChangeEvent): void { - that._onDidChangeCellOutputs.fire(event); - }, - emitCellLanguageChange(event: vscode.NotebookCellLanguageChangeEvent): void { - that._onDidChangeCellLanguage.fire(event); - }, - emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void { - that._onDidChangeCellMetadata.fire(event); - }, - }, viewType, revivedUri, this, storageRoot); - this._unInitializedDocuments.set(revivedUri.toString(), document); - } - - const rawCells = await provider.provider.openNotebook(URI.revive(uri), { backupId }); - const dto = { - metadata: { - ...notebookDocumentMetadataDefaults, - ...rawCells.metadata - }, - languages: rawCells.languages, - cells: rawCells.cells.map(cell => ({ - ...cell, - outputs: cell.outputs.map(o => addIdToOutput(o)) - })), - }; - - return dto; + if (!provider) { + return; } - return; + const storageRoot = this._extensionStoragePaths.workspaceValue(provider.extension) ?? this._extensionStoragePaths.globalValue(provider.extension); + let document = this._documents.get(revivedUri); + + if (!document) { + const that = this; + document = this._unInitializedDocuments.get(revivedUri) ?? new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, { + emitModelChange(event: vscode.NotebookCellsChangeEvent): void { + that._onDidChangeNotebookCells.fire(event); + }, + emitCellOutputsChange(event: vscode.NotebookCellOutputsChangeEvent): void { + that._onDidChangeCellOutputs.fire(event); + }, + emitCellLanguageChange(event: vscode.NotebookCellLanguageChangeEvent): void { + that._onDidChangeCellLanguage.fire(event); + }, + emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void { + that._onDidChangeCellMetadata.fire(event); + }, + }, viewType, revivedUri, this, storageRoot); + this._unInitializedDocuments.set(revivedUri, document); + } + + const rawCells = await provider.provider.openNotebook(URI.revive(uri), { backupId }); + const dto = { + metadata: { + ...notebookDocumentMetadataDefaults, + ...rawCells.metadata + }, + languages: rawCells.languages, + cells: rawCells.cells.map(cell => ({ + ...cell, + outputs: cell.outputs.map(o => addIdToOutput(o)) + })), + }; + + return dto; } async $resolveNotebookEditor(viewType: string, uri: UriComponents, editorId: string): Promise { const provider = this._notebookContentProviders.get(viewType); const revivedUri = URI.revive(uri); - const document = this._documents.get(revivedUri.toString()); + const document = this._documents.get(revivedUri); if (!document || !provider) { return; } @@ -1245,69 +1142,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return; } - await provider.provider.resolveNotebook(document, webComm.contentProviderComm); - } - - private provideCommToNotebookRenderers(document: ExtHostNotebookDocument, renderer: ExtHostNotebookOutputRenderer) { - let alreadyRegistered = this._renderersUsedInNotebooks.get(document); - if (!alreadyRegistered) { - alreadyRegistered = new Set(); - this._renderersUsedInNotebooks.set(document, alreadyRegistered); - } - - if (alreadyRegistered.has(renderer)) { - return; - } - - alreadyRegistered.add(renderer); - for (const editorId of this._editors.keys()) { - const comm = this._webviewComm.get(editorId); - if (comm) { - renderer.resolveNotebook(document, comm); - } - } - } - - async $executeNotebookByAttachedKernel(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise { - const document = this._documents.get(URI.revive(uri).toString()); - - if (!document) { - return; - } - - if (this._notebookContentProviders.has(viewType)) { - const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; - const provider = this._notebookContentProviders.get(viewType)!.provider; - - if (provider.kernel) { - if (cell) { - return withToken(token => (provider.kernel!.executeCell as any)(document, cell, token)); - } else { - return withToken(token => (provider.kernel!.executeAllCells as any)(document, token)); - } - } - } - } - - async $cancelNotebookByAttachedKernel(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise { - const document = this._documents.get(URI.revive(uri).toString()); - - if (!document) { - return; - } - - if (this._notebookContentProviders.has(viewType)) { - const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; - const provider = this._notebookContentProviders.get(viewType)!.provider; - - if (provider.kernel) { - if (cell) { - return provider.kernel.cancelCellExecution(document, cell); - } else { - return provider.kernel.cancelAllCellsExecution(document); - } - } - } + await provider.provider.resolveNotebook(document.notebookDocument, webComm.contentProviderComm); } async $executeNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined): Promise { @@ -1327,9 +1162,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } async $executeNotebook2(kernelId: string, viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise { - const document = this._documents.get(URI.revive(uri).toString()); + const document = this._documents.get(URI.revive(uri)); - if (!document || document.viewType !== viewType) { + if (!document || document.notebookDocument.viewType !== viewType) { return; } @@ -1342,20 +1177,20 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; if (cell) { - return withToken(token => (kernelInfo!.kernel.executeCell as any)(document, cell, token)); + return withToken(token => (kernelInfo!.kernel.executeCell as any)(document.notebookDocument, cell.cell, token)); } else { - return withToken(token => (kernelInfo!.kernel.executeAllCells as any)(document, token)); + return withToken(token => (kernelInfo!.kernel.executeAllCells as any)(document.notebookDocument, token)); } } async $saveNotebook(viewType: string, uri: UriComponents, token: CancellationToken): Promise { - const document = this._documents.get(URI.revive(uri).toString()); + const document = this._documents.get(URI.revive(uri)); if (!document) { return false; } if (this._notebookContentProviders.has(viewType)) { - await this._notebookContentProviders.get(viewType)!.provider.saveNotebook(document, token); + await this._notebookContentProviders.get(viewType)!.provider.saveNotebook(document.notebookDocument, token); return true; } @@ -1363,13 +1198,13 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } async $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise { - const document = this._documents.get(URI.revive(uri).toString()); + const document = this._documents.get(URI.revive(uri)); if (!document) { return false; } if (this._notebookContentProviders.has(viewType)) { - await this._notebookContentProviders.get(viewType)!.provider.saveNotebookAs(URI.revive(target), document, token); + await this._notebookContentProviders.get(viewType)!.provider.saveNotebookAs(URI.revive(target), document.notebookDocument, token); return true; } @@ -1377,7 +1212,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } async $undoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise { - const document = this._documents.get(URI.revive(uri).toString()); + const document = this._documents.get(URI.revive(uri)); if (!document) { return; } @@ -1387,7 +1222,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } async $redoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise { - const document = this._documents.get(URI.revive(uri).toString()); + const document = this._documents.get(URI.revive(uri)); if (!document) { return; } @@ -1397,11 +1232,11 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN async $backup(viewType: string, uri: UriComponents, cancellation: CancellationToken): Promise { - const document = this._documents.get(URI.revive(uri).toString()); + const document = this._documents.get(URI.revive(uri)); const provider = this._notebookContentProviders.get(viewType); if (document && provider && provider.provider.backupNotebook) { - const backup = await provider.provider.backupNotebook(document, { destination: document.getNewBackupUri() }, cancellation); + const backup = await provider.provider.backupNotebook(document.notebookDocument, { destination: document.getNewBackupUri() }, cancellation); document.updateBackup(backup); return backup.id; } @@ -1418,11 +1253,11 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._withAdapter(event.providerHandle, event.uri, async (adapter, document) => { const kernel = event.kernelId ? adapter.getKernel(event.kernelId) : undefined; this._editors.forEach(editor => { - if (editor.editor.document === document) { + if (editor.editor.notebookData === document) { editor.editor.updateActiveKernel(kernel); } }); - this._onDidChangeActiveNotebookKernel.fire({ document, kernel }); + this._onDidChangeActiveNotebookKernel.fire({ document: document.notebookDocument, kernel }); }); } } @@ -1444,23 +1279,46 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._webviewComm.get(editorId)?.onDidReceiveMessage(forRendererType, message); } - $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent): void { - const document = this._documents.get(URI.revive(uriComponents).toString()); - + $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent, isDirty: boolean): void { + const document = this._documents.get(URI.revive(uriComponents)); if (document) { - document.acceptModelChanged(event); + document.acceptModelChanged(event, isDirty); } } public $acceptModelSaved(uriComponents: UriComponents): void { - const document = this._documents.get(URI.revive(uriComponents).toString()); + const document = this._documents.get(URI.revive(uriComponents)); if (document) { // this.$acceptDirtyStateChanged(uriComponents, false); - this._onDidSaveNotebookDocument.fire(document); + this._onDidSaveNotebookDocument.fire(document.notebookDocument); } } - $acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void { + $acceptEditorPropertiesChanged(id: string, data: INotebookEditorPropertiesChangeData): void { + this.logService.debug('ExtHostNotebook#$acceptEditorPropertiesChanged', id, data); + + let editor: { editor: ExtHostNotebookEditor; } | undefined; + this._editors.forEach(e => { + if (e.editor.id === id) { + editor = e; + } + }); + + if (!editor) { + return; + } + + if (data.visibleRanges) { + editor.editor._acceptVisibleRanges(data.visibleRanges.ranges); + this._onDidChangeNotebookEditorVisibleRanges.fire({ + notebookEditor: editor.editor, + visibleRanges: editor.editor.visibleRanges + }); + } + } + + $acceptDocumentPropertiesChanged(uriComponents: UriComponents, data: INotebookDocumentPropertiesChangeData): void { + this.logService.debug('ExtHostNotebook#$acceptDocumentPropertiesChanged', uriComponents.path, data); const editor = this._getEditorFromURI(uriComponents); if (!editor) { @@ -1468,25 +1326,29 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } if (data.selections) { - const cells = editor.editor.document.cells; - if (data.selections.selections.length) { const firstCell = data.selections.selections[0]; - editor.editor.selection = cells.find(cell => cell.handle === firstCell); + editor.editor.selection = editor.editor.notebookData.getCell(firstCell)?.cell; } else { editor.editor.selection = undefined; } + + this._onDidChangeNotebookEditorSelection.fire({ + notebookEditor: editor.editor, + selection: editor.editor.selection + }); } + if (data.metadata) { - editor.editor.document.metadata = { + editor.editor.notebookData.notebookDocument.metadata = { ...notebookDocumentMetadataDefaults, ...data.metadata }; } } - private _createExtHostEditor(document: ExtHostNotebookDocument, editorId: string, selections: number[]) { + private _createExtHostEditor(document: ExtHostNotebookDocument, editorId: string, selections: number[], visibleRanges: vscode.NotebookCellRange[]) { const revivedUri = document.uri; let webComm = this._webviewComm.get(editorId); @@ -1496,7 +1358,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } const editor = new ExtHostNotebookEditor( - document.viewType, + document.notebookDocument.viewType, editorId, revivedUri, this._proxy, @@ -1504,64 +1366,59 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN document ); - const cells = editor.document.cells; - if (selections.length) { const firstCell = selections[0]; - editor.selection = cells.find(cell => cell.handle === firstCell); + editor.selection = editor.notebookData.getCell(firstCell)?.cell; } else { editor.selection = undefined; } + editor._acceptVisibleRanges(visibleRanges); + this._editors.get(editorId)?.editor.dispose(); - - for (const renderer of this._renderersUsedInNotebooks.get(document) ?? []) { - renderer.resolveNotebook(document, webComm); - } - this._editors.set(editorId, { editor }); } - async $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta) { + $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): void { let editorChanged = false; if (delta.removedDocuments) { - delta.removedDocuments.forEach((uri) => { + for (const uri of delta.removedDocuments) { const revivedUri = URI.revive(uri); - const revivedUriStr = revivedUri.toString(); - const document = this._documents.get(revivedUriStr); + const document = this._documents.get(revivedUri); if (document) { document.dispose(); - this._documents.delete(revivedUriStr); - this._onDidCloseNotebookDocument.fire(document); + this._documents.delete(revivedUri); + this._documentsAndEditors.$acceptDocumentsAndEditorsDelta({ removedDocuments: document.notebookDocument.cells.map(cell => cell.uri) }); + this._onDidCloseNotebookDocument.fire(document.notebookDocument); } - [...this._editors.values()].forEach((e) => { - if (e.editor.uri.toString() === revivedUriStr) { + for (const e of this._editors.values()) { + if (e.editor.uri.toString() === revivedUri.toString()) { e.editor.dispose(); this._editors.delete(e.editor.id); editorChanged = true; } - }); - }); + } + } } if (delta.addedDocuments) { - delta.addedDocuments.forEach(modelData => { + + const addedCellDocuments: IModelAddedData[] = []; + + for (const modelData of delta.addedDocuments) { const revivedUri = URI.revive(modelData.uri); - const revivedUriStr = revivedUri.toString(); const viewType = modelData.viewType; const entry = this._notebookContentProviders.get(viewType); - let storageRoot: URI | undefined; - if (entry && this._extensionStoragePaths) { - storageRoot = this._extensionStoragePaths.workspaceValue(entry.extension) ?? this._extensionStoragePaths.globalValue(entry.extension); - } + const storageRoot = entry && (this._extensionStoragePaths.workspaceValue(entry.extension) ?? this._extensionStoragePaths.globalValue(entry.extension)); - if (!this._documents.has(revivedUriStr)) { + + if (!this._documents.has(revivedUri)) { const that = this; - const document = this._unInitializedDocuments.get(revivedUriStr) ?? new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, { + const document = this._unInitializedDocuments.get(revivedUri) ?? new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, { emitModelChange(event: vscode.NotebookCellsChangeEvent): void { that._onDidChangeNotebookCells.fire(event); }, @@ -1576,9 +1433,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } }, viewType, revivedUri, this, storageRoot); - this._unInitializedDocuments.delete(revivedUriStr); + this._unInitializedDocuments.delete(revivedUri); if (modelData.metadata) { - document.metadata = { + document.notebookDocument.metadata = { ...notebookDocumentMetadataDefaults, ...modelData.metadata }; @@ -1592,43 +1449,48 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN 0, modelData.cells ]] - }); + }, false); - this._documents.get(revivedUriStr)?.dispose(); - this._documents.set(revivedUriStr, document); + // add cell document as vscode.TextDocument + addedCellDocuments.push(...modelData.cells.map(cell => ExtHostCell.asModelAddData(document.notebookDocument, cell))); + + this._documents.get(revivedUri)?.dispose(); + this._documents.set(revivedUri, document); // create editor if populated if (modelData.attachedEditor) { - this._createExtHostEditor(document, modelData.attachedEditor.id, modelData.attachedEditor.selections); + this._createExtHostEditor(document, modelData.attachedEditor.id, modelData.attachedEditor.selections, modelData.attachedEditor.visibleRanges); editorChanged = true; } } - const document = this._documents.get(revivedUriStr)!; - this._onDidOpenNotebookDocument.fire(document); - }); + this._documentsAndEditors.$acceptDocumentsAndEditorsDelta({ addedDocuments: addedCellDocuments }); + + const document = this._documents.get(revivedUri)!; + this._onDidOpenNotebookDocument.fire(document.notebookDocument); + } } if (delta.addedEditors) { - delta.addedEditors.forEach(editorModelData => { + for (const editorModelData of delta.addedEditors) { if (this._editors.has(editorModelData.id)) { return; } const revivedUri = URI.revive(editorModelData.documentUri); - const document = this._documents.get(revivedUri.toString()); + const document = this._documents.get(revivedUri); if (document) { - this._createExtHostEditor(document, editorModelData.id, editorModelData.selections); + this._createExtHostEditor(document, editorModelData.id, editorModelData.selections, editorModelData.visibleRanges); editorChanged = true; } - }); + } } const removedEditors: { editor: ExtHostNotebookEditor; }[] = []; if (delta.removedEditors) { - delta.removedEditors.forEach(editorid => { + for (const editorid of delta.removedEditors) { const editor = this._editors.get(editorid); if (editor) { @@ -1641,7 +1503,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN removedEditors.push(editor); } - }); + } } if (editorChanged) { @@ -1655,10 +1517,10 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const visibleEditorsSet = new Set(); this.visibleNotebookEditors.forEach(editor => visibleEditorsSet.add(editor.id)); - [...this._editors.values()].forEach((e) => { + for (const e of this._editors.values()) { const newValue = visibleEditorsSet.has(e.editor.id); e.editor._acceptVisibility(newValue); - }); + } this.visibleNotebookEditors = [...this._editors.values()].map(e => e.editor).filter(e => e.visible); this._onDidChangeVisibleNotebookEditors.fire(this.visibleNotebookEditors); @@ -1668,24 +1530,40 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN if (delta.newActiveEditor) { this._activeNotebookEditor = this._editors.get(delta.newActiveEditor)?.editor; this._activeNotebookEditor?._acceptActive(true); - [...this._editors.values()].forEach((e) => { + for (const e of this._editors.values()) { if (e.editor !== this.activeNotebookEditor) { e.editor._acceptActive(false); } - }); + } } else { // clear active notebook as current active editor is non-notebook editor this._activeNotebookEditor = undefined; - - [...this._editors.values()].forEach((e) => { + for (const e of this._editors.values()) { e.editor._acceptActive(false); - }); - + } } this._onDidChangeActiveNotebookEditor.fire(this._activeNotebookEditor); } } + + createNotebookCellStatusBarItemInternal(cell: vscode.NotebookCell, alignment: extHostTypes.NotebookCellStatusBarAlignment | undefined, priority: number | undefined) { + const statusBarItem = new NotebookCellStatusBarItemInternal(this._proxy, this._commandsConverter, cell, alignment, priority); + + // Look up the ExtHostCell for this NotebookCell URI, bind to its disposable lifecycle + const parsedUri = CellUri.parse(cell.uri); + if (parsedUri) { + const document = this._documents.get(parsedUri.notebook); + if (document) { + const cell = document.getCell(parsedUri.handle); + if (cell) { + Event.once(cell.onDidDispose)(() => statusBarItem.dispose()); + } + } + } + + return statusBarItem; + } } function hashPath(resource: URI): string { @@ -1697,3 +1575,178 @@ function isEditEvent(e: vscode.NotebookDocumentEditEvent | vscode.NotebookDocume return typeof (e as vscode.NotebookDocumentEditEvent).undo === 'function' && typeof (e as vscode.NotebookDocumentEditEvent).redo === 'function'; } + +export class NotebookCellStatusBarItemInternal extends Disposable { + private static NEXT_ID = 0; + + private readonly _id = NotebookCellStatusBarItemInternal.NEXT_ID++; + private readonly _internalCommandRegistration: DisposableStore; + + private _isDisposed = false; + private _alignment: extHostTypes.NotebookCellStatusBarAlignment; + + constructor( + private readonly _proxy: MainThreadNotebookShape, + private readonly _commands: CommandsConverter, + private readonly _cell: vscode.NotebookCell, + alignment: extHostTypes.NotebookCellStatusBarAlignment | undefined, + private _priority: number | undefined) { + super(); + this._internalCommandRegistration = this._register(new DisposableStore()); + this._alignment = alignment ?? extHostTypes.NotebookCellStatusBarAlignment.Left; + } + + private _apiItem: vscode.NotebookCellStatusBarItem | undefined; + get apiItem(): vscode.NotebookCellStatusBarItem { + if (!this._apiItem) { + this._apiItem = createNotebookCellStatusBarApiItem(this); + } + + return this._apiItem; + } + + get cell(): vscode.NotebookCell { + return this._cell; + } + + get alignment(): extHostTypes.NotebookCellStatusBarAlignment { + return this._alignment; + } + + set alignment(v: extHostTypes.NotebookCellStatusBarAlignment) { + this._alignment = v; + this.update(); + } + + get priority(): number | undefined { + return this._priority; + } + + set priority(v: number | undefined) { + this._priority = v; + this.update(); + } + + private _text: string = ''; + get text(): string { + return this._text; + } + + set text(v: string) { + this._text = v; + this.update(); + } + + private _tooltip: string | undefined; + get tooltip(): string | undefined { + return this._tooltip; + } + + set tooltip(v: string | undefined) { + this._tooltip = v; + this.update(); + } + + private _command?: { + readonly fromApi: string | vscode.Command, + readonly internal: ICommandDto, + }; + get command(): string | vscode.Command | undefined { + return this._command?.fromApi; + } + + set command(command: string | vscode.Command | undefined) { + if (this._command?.fromApi === command) { + return; + } + + this._internalCommandRegistration.clear(); + if (typeof command === 'string') { + this._command = { + fromApi: command, + internal: this._commands.toInternal({ title: '', command }, this._internalCommandRegistration), + }; + } else if (command) { + this._command = { + fromApi: command, + internal: this._commands.toInternal(command, this._internalCommandRegistration), + }; + } else { + this._command = undefined; + } + this.update(); + } + + private _accessibilityInformation: vscode.AccessibilityInformation | undefined; + get accessibilityInformation(): vscode.AccessibilityInformation | undefined { + return this._accessibilityInformation; + } + + set accessibilityInformation(v: vscode.AccessibilityInformation | undefined) { + this._accessibilityInformation = v; + this.update(); + } + + private _visible: boolean = false; + show(): void { + this._visible = true; + this.update(); + } + + hide(): void { + this._visible = false; + this.update(); + } + + dispose(): void { + this.hide(); + this._isDisposed = true; + this._internalCommandRegistration.dispose(); + } + + private update(): void { + if (this._isDisposed) { + return; + } + + const entry: INotebookCellStatusBarEntry = { + alignment: this.alignment === extHostTypes.NotebookCellStatusBarAlignment.Left ? CellStatusbarAlignment.LEFT : CellStatusbarAlignment.RIGHT, + cellResource: this.cell.uri, + command: this._command?.internal, + text: this.text, + tooltip: this.tooltip, + accessibilityInformation: this.accessibilityInformation, + priority: this.priority, + visible: this._visible + }; + + this._proxy.$setStatusBarEntry(this._id, entry); + } +} + +function createNotebookCellStatusBarApiItem(internalItem: NotebookCellStatusBarItemInternal): vscode.NotebookCellStatusBarItem { + return Object.freeze({ + cell: internalItem.cell, + get alignment() { return internalItem.alignment; }, + set alignment(v: NotebookCellStatusBarItemInternal['alignment']) { internalItem.alignment = v; }, + + get priority() { return internalItem.priority; }, + set priority(v: NotebookCellStatusBarItemInternal['priority']) { internalItem.priority = v; }, + + get text() { return internalItem.text; }, + set text(v: NotebookCellStatusBarItemInternal['text']) { internalItem.text = v; }, + + get tooltip() { return internalItem.tooltip; }, + set tooltip(v: NotebookCellStatusBarItemInternal['tooltip']) { internalItem.tooltip = v; }, + + get command() { return internalItem.command; }, + set command(v: NotebookCellStatusBarItemInternal['command']) { internalItem.command = v; }, + + get accessibilityInformation() { return internalItem.accessibilityInformation; }, + set accessibilityInformation(v: NotebookCellStatusBarItemInternal['accessibilityInformation']) { internalItem.accessibilityInformation = v; }, + + show() { internalItem.show(); }, + hide() { internalItem.hide(); }, + dispose() { internalItem.dispose(); } + }); +} diff --git a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts index 8ffeda97be3..4702e1c8165 100644 --- a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts @@ -6,7 +6,7 @@ import * as types from 'vs/workbench/api/common/extHostTypes'; import * as vscode from 'vscode'; import { Event, Emitter } from 'vs/base/common/event'; -import { ExtHostNotebookController, ExtHostCell } from 'vs/workbench/api/common/extHostNotebook'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; import { DisposableStore } from 'vs/base/common/lifecycle'; @@ -21,7 +21,7 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD private _disposables = new DisposableStore(); private _isClosed = false; - private _cells!: ExtHostCell[]; + private _cells!: vscode.NotebookCell[]; private _cellUris!: ResourceMap; private _cellLengths!: PrefixSumComputer; private _cellLines!: PrefixSumComputer; @@ -78,7 +78,7 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD for (const cell of this._notebook.cells) { if (cell.cellKind === CellKind.Code && (!this._selector || score(this._selector, cell.uri, cell.language, true))) { this._cellUris.set(cell.uri, this._cells.length); - this._cells.push(cell); + this._cells.push(cell); cellLengths.push(cell.document.getText().length + 1); cellLineCounts.push(cell.document.lineCount); } diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 77d142d2bd6..83739e744dd 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -516,6 +516,7 @@ class ExtHostSourceControl implements vscode.SourceControl { } this._proxy.$registerGroups(this.handle, groups, splices); + this.createdResourceGroups.clear(); } @debounce(100) diff --git a/src/vs/workbench/api/common/extHostTask.ts b/src/vs/workbench/api/common/extHostTask.ts index ee6423408fd..3fb57723234 100644 --- a/src/vs/workbench/api/common/extHostTask.ts +++ b/src/vs/workbench/api/common/extHostTask.ts @@ -192,12 +192,16 @@ export namespace CustomExecutionDTO { export namespace TaskHandleDTO { - export function from(value: types.Task): tasks.TaskHandleDTO { + export function from(value: types.Task, workspaceService?: IExtHostWorkspace): tasks.TaskHandleDTO { let folder: UriComponents | string; if (value.scope !== undefined && typeof value.scope !== 'number') { folder = value.scope.uri; } else if (value.scope !== undefined && typeof value.scope === 'number') { - folder = USER_TASKS_GROUP_KEY; + if ((value.scope === types.TaskScope.Workspace) && workspaceService && workspaceService.workspaceFile) { + folder = workspaceService.workspaceFile; + } else { + folder = USER_TASKS_GROUP_KEY; + } } return { id: value._id!, @@ -265,8 +269,8 @@ export namespace TaskDTO { presentationOptions: TaskPresentationOptionsDTO.from(value.presentationOptions), problemMatchers: value.problemMatchers, hasDefinedMatchers: (value as types.Task).hasDefinedMatchers, - runOptions: (value).runOptions ? (value).runOptions : { reevaluateOnRerun: true }, - detail: (value).detail + runOptions: value.runOptions ? value.runOptions : { reevaluateOnRerun: true }, + detail: value.detail }; return result; } @@ -471,11 +475,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask return this._onDidExecuteTask.event; } - protected async resolveDefinition(uri: number | UriComponents | undefined, definition: vscode.TaskDefinition | undefined): Promise { - return definition; - } - - public async $onDidStartTask(execution: tasks.TaskExecutionDTO, terminalId: number): Promise { + public async $onDidStartTask(execution: tasks.TaskExecutionDTO, terminalId: number, resolvedDefinition: tasks.TaskDefinitionDTO): Promise { const customExecution: types.CustomExecution | undefined = this._providedCustomExecutions2.get(execution.id); if (customExecution) { if (this._activeCustomExecutions2.get(execution.id) !== undefined) { @@ -484,7 +484,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask // Clone the custom execution to keep the original untouched. This is important for multiple runs of the same task. this._activeCustomExecutions2.set(execution.id, customExecution); - this._terminalService.attachPtyToTerminal(terminalId, await customExecution.callback(await this.resolveDefinition(execution.task?.source.scope, execution.task?.definition))); + this._terminalService.attachPtyToTerminal(terminalId, await customExecution.callback(resolvedDefinition)); } this._lastStartedTask = execution.id; diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index ed8fd570cdd..7107003f8f7 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -41,7 +41,6 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape { attachPtyToTerminal(id: number, pty: vscode.Pseudoterminal): void; getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string; getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string; - registerLinkHandler(handler: vscode.TerminalLinkHandler): vscode.Disposable; registerLinkProvider(provider: vscode.TerminalLinkProvider): vscode.Disposable; getEnvironmentVariableCollection(extension: IExtensionDescription, persistent?: boolean): vscode.EnvironmentVariableCollection; } @@ -176,6 +175,9 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi // Nothing changed return false; } + if (cols === 0 || rows === 0) { + return false; + } this._cols = cols; this._rows = rows; return true; @@ -318,7 +320,6 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ protected _environmentVariableCollections: Map = new Map(); private readonly _bufferer: TerminalDataBufferer; - private readonly _linkHandlers: Set = new Set(); private readonly _linkProviders: Set = new Set(); private readonly _terminalLinkCache: Map> = new Map(); private readonly _terminalLinkCancellationSource: Map = new Map(); @@ -338,6 +339,7 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ public get onDidWriteTerminalData(): Event { return this._onDidWriteTerminalData && this._onDidWriteTerminalData.event; } constructor( + supportsProcesses: boolean, @IExtHostRpcService extHostRpc: IExtHostRpcService ) { this._proxy = extHostRpc.getProxy(MainContext.MainThreadTerminalService); @@ -346,6 +348,7 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ onFirstListenerAdd: () => this._proxy.$startSendingDataEvents(), onLastListenerRemove: () => this._proxy.$stopSendingDataEvents() }); + this._proxy.$registerProcessSupport(supportsProcesses); } public abstract createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal; @@ -559,19 +562,6 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ return id; } - public registerLinkHandler(handler: vscode.TerminalLinkHandler): vscode.Disposable { - this._linkHandlers.add(handler); - if (this._linkHandlers.size === 1 && this._linkProviders.size === 0) { - this._proxy.$startHandlingLinks(); - } - return new VSCodeDisposable(() => { - this._linkHandlers.delete(handler); - if (this._linkHandlers.size === 0 && this._linkProviders.size === 0) { - this._proxy.$stopHandlingLinks(); - } - }); - } - public registerLinkProvider(provider: vscode.TerminalLinkProvider): vscode.Disposable { this._linkProviders.add(provider); if (this._linkProviders.size === 1) { @@ -585,25 +575,6 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ }); } - public async $handleLink(id: number, link: string): Promise { - const terminal = this._getTerminalById(id); - if (!terminal) { - return false; - } - - // Call each handler synchronously so multiple handlers aren't triggered at once - const it = this._linkHandlers.values(); - let next = it.next(); - while (!next.done) { - const handled = await next.value.handleLink(terminal, link); - if (handled) { - return true; - } - next = it.next(); - } - return false; - } - public async $provideLinks(terminalId: number, line: string): Promise { const terminal = this._getTerminalById(terminalId); if (!terminal) { @@ -836,6 +807,12 @@ export class EnvironmentVariableCollection implements vscode.EnvironmentVariable } export class WorkerExtHostTerminalService extends BaseExtHostTerminalService { + constructor( + @IExtHostRpcService extHostRpc: IExtHostRpcService + ) { + super(false, extHostRpc); + } + public createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal { throw new NotSupportedError(); } diff --git a/src/vs/workbench/api/common/extHostTextEditors.ts b/src/vs/workbench/api/common/extHostTextEditors.ts index e99f513e940..a2ba800bf5c 100644 --- a/src/vs/workbench/api/common/extHostTextEditors.ts +++ b/src/vs/workbench/api/common/extHostTextEditors.ts @@ -7,6 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import * as arrays from 'vs/base/common/arrays'; import { ExtHostEditorsShape, IEditorPropertiesChangeData, IMainContext, ITextDocumentShowOptions, ITextEditorPositionData, MainContext, MainThreadTextEditorsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; import { ExtHostTextEditor, TextEditorDecorationType } from 'vs/workbench/api/common/extHostTextEditor'; import * as TypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { TextEditorSelectionChangeKind } from 'vs/workbench/api/common/extHostTypes'; @@ -28,16 +29,15 @@ export class ExtHostEditors implements ExtHostEditorsShape { readonly onDidChangeActiveTextEditor: Event = this._onDidChangeActiveTextEditor.event; readonly onDidChangeVisibleTextEditors: Event = this._onDidChangeVisibleTextEditors.event; - - private _proxy: MainThreadTextEditorsShape; - private _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors; + private readonly _proxy: MainThreadTextEditorsShape; constructor( mainContext: IMainContext, - extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, + private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, + private readonly _extHostNotebooks: ExtHostNotebookController, ) { this._proxy = mainContext.getProxy(MainContext.MainThreadTextEditors); - this._extHostDocumentsAndEditors = extHostDocumentsAndEditors; + this._extHostDocumentsAndEditors.onDidChangeVisibleTextEditors(e => this._onDidChangeVisibleTextEditors.fire(e)); this._extHostDocumentsAndEditors.onDidChangeActiveTextEditor(e => this._onDidChangeActiveTextEditor.fire(e)); @@ -93,7 +93,7 @@ export class ExtHostEditors implements ExtHostEditorsShape { } applyWorkspaceEdit(edit: vscode.WorkspaceEdit): Promise { - const dto = TypeConverters.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors); + const dto = TypeConverters.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors, this._extHostNotebooks); return this._proxy.$tryApplyWorkspaceEdit(dto); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 6a7993f9ae7..18c431d0f83 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -31,6 +31,7 @@ import { LogLevel as _MainLogLevel } from 'vs/platform/log/common/log'; import { coalesce, isNonEmptyArray } from 'vs/base/common/arrays'; import { RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; export interface PositionLike { line: number; @@ -131,8 +132,9 @@ export namespace DiagnosticTag { return types.DiagnosticTag.Unnecessary; case MarkerTag.Deprecated: return types.DiagnosticTag.Deprecated; + default: + return undefined; } - return undefined; } } @@ -210,8 +212,9 @@ export namespace DiagnosticSeverity { return types.DiagnosticSeverity.Error; case MarkerSeverity.Hint: return types.DiagnosticSeverity.Hint; + default: + return types.DiagnosticSeverity.Error; } - return types.DiagnosticSeverity.Error; } } @@ -503,32 +506,42 @@ export namespace TextEdit { } export namespace WorkspaceEdit { - export function from(value: vscode.WorkspaceEdit, documents?: ExtHostDocumentsAndEditors): extHostProtocol.IWorkspaceEditDto { + export function from(value: vscode.WorkspaceEdit, documents?: ExtHostDocumentsAndEditors, notebooks?: ExtHostNotebookController): extHostProtocol.IWorkspaceEditDto { const result: extHostProtocol.IWorkspaceEditDto = { edits: [] }; if (value instanceof types.WorkspaceEdit) { - for (let entry of value.allEntries()) { + for (let entry of value._allEntries()) { - if (entry._type === 1) { + if (entry._type === types.FileEditType.File) { // file operation result.edits.push({ + _type: extHostProtocol.WorkspaceEditType.File, oldUri: entry.from, newUri: entry.to, options: entry.options, metadata: entry.metadata }); - } else { + } else if (entry._type === types.FileEditType.Text) { // text edits const doc = documents?.getDocument(entry.uri); result.edits.push({ + _type: extHostProtocol.WorkspaceEditType.Text, resource: entry.uri, edit: TextEdit.from(entry.edit), modelVersionId: doc?.version, metadata: entry.metadata }); + } else if (entry._type === types.FileEditType.Cell) { + result.edits.push({ + _type: extHostProtocol.WorkspaceEditType.Cell, + resource: entry.uri, + edit: entry.edit, + metadata: entry.metadata, + modelVersionId: notebooks?.lookupNotebookDocument(entry.uri)?.notebookDocument.version + }); } } } @@ -1253,9 +1266,9 @@ export namespace LogLevel { return _MainLogLevel.Critical; case types.LogLevel.Off: return _MainLogLevel.Off; + default: + return _MainLogLevel.Info; } - - return _MainLogLevel.Info; } export function to(mainLevel: _MainLogLevel): types.LogLevel { @@ -1274,8 +1287,59 @@ export namespace LogLevel { return types.LogLevel.Critical; case _MainLogLevel.Off: return types.LogLevel.Off; + default: + return types.LogLevel.Info; } - - return types.LogLevel.Info; + } +} + +export namespace NotebookExclusiveDocumentPattern { + export function from(pattern: { include: vscode.GlobPattern | undefined, exclude: vscode.GlobPattern | undefined }): { include: string | types.RelativePattern | undefined, exclude: string | types.RelativePattern | undefined }; + export function from(pattern: vscode.GlobPattern): string | types.RelativePattern; + export function from(pattern: undefined): undefined; + export function from(pattern: { include: vscode.GlobPattern | undefined | null, exclude: vscode.GlobPattern | undefined } | vscode.GlobPattern | undefined): string | types.RelativePattern | { include: string | types.RelativePattern | undefined, exclude: string | types.RelativePattern | undefined } | undefined; + export function from(pattern: { include: vscode.GlobPattern | undefined | null, exclude: vscode.GlobPattern | undefined } | vscode.GlobPattern | undefined): string | types.RelativePattern | { include: string | types.RelativePattern | undefined, exclude: string | types.RelativePattern | undefined } | undefined { + if (pattern instanceof types.RelativePattern) { + return pattern; + } + + if (typeof pattern === 'string') { + return pattern; + } + + + if (isRelativePattern(pattern)) { + return new types.RelativePattern(pattern.base, pattern.pattern); + } + + if (isExclusivePattern(pattern)) { + return { + include: GlobPattern.from(pattern.include) || undefined, + exclude: GlobPattern.from(pattern.exclude) || undefined + }; + } + + return undefined; // preserve `undefined` + + } + + function isExclusivePattern(obj: any): obj is { include: types.RelativePattern | undefined | null, exclude: types.RelativePattern | undefined | null } { + const ep = obj as { include: vscode.GlobPattern, exclude: vscode.GlobPattern }; + const include = GlobPattern.from(ep.include); + if (!(include && include instanceof types.RelativePattern || typeof include === 'string')) { + return false; + } + + const exclude = GlobPattern.from(ep.exclude); + if (!(exclude && exclude instanceof types.RelativePattern || typeof exclude === 'string')) { + return false; + } + + return true; + } + + function isRelativePattern(obj: any): obj is vscode.RelativePattern { + const rp = obj as vscode.RelativePattern; + return rp && typeof rp.base === 'string' && typeof rp.pattern === 'string'; } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 447fa8092c6..d832779db22 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3,17 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce, equals } from 'vs/base/common/arrays'; +import { coalesceInPlace, equals } from 'vs/base/common/arrays'; import { escapeCodicons } from 'vs/base/common/codicons'; import { illegalArgument } from 'vs/base/common/errors'; import { IRelativePattern } from 'vs/base/common/glob'; import { isMarkdownString } from 'vs/base/common/htmlContent'; +import { ResourceMap } from 'vs/base/common/map'; import { startsWith } from 'vs/base/common/strings'; import { isStringArray } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files'; import { RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { addIdToOutput, CellEditType, ICellEditOperation } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import type * as vscode from 'vscode'; function es5ClassCompat(target: Function): any { @@ -575,8 +577,14 @@ export interface IFileOperationOptions { recursive?: boolean; } +export const enum FileEditType { + File = 1, + Text = 2, + Cell = 3 +} + export interface IFileOperation { - _type: 1; + _type: FileEditType.File; from?: URI; to?: URI; options?: IFileOperationOptions; @@ -584,31 +592,61 @@ export interface IFileOperation { } export interface IFileTextEdit { - _type: 2; + _type: FileEditType.Text; uri: URI; edit: TextEdit; metadata?: vscode.WorkspaceEditEntryMetadata; } +export interface IFileCellEdit { + _type: FileEditType.Cell; + uri: URI; + edit: ICellEditOperation; + metadata?: vscode.WorkspaceEditEntryMetadata; +} + @es5ClassCompat export class WorkspaceEdit implements vscode.WorkspaceEdit { - private _edits = new Array(); + private readonly _edits = new Array(); + + + _allEntries(): ReadonlyArray { + return this._edits; + } + + // --- file renameFile(from: vscode.Uri, to: vscode.Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean; }, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: 1, from, to, options, metadata }); + this._edits.push({ _type: FileEditType.File, from, to, options, metadata }); } createFile(uri: vscode.Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean; }, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: 1, from: undefined, to: uri, options, metadata }); + this._edits.push({ _type: FileEditType.File, from: undefined, to: uri, options, metadata }); } deleteFile(uri: vscode.Uri, options?: { recursive?: boolean, ignoreIfNotExists?: boolean; }, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: 1, from: uri, to: undefined, options, metadata }); + this._edits.push({ _type: FileEditType.File, from: uri, to: undefined, options, metadata }); } + // --- cell + + replaceCells(uri: URI, start: number, end: number, cells: vscode.NotebookCellData[], metadata?: vscode.WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.Replace, index: start, count: end - start, cells: cells.map(cell => ({ ...cell, outputs: cell.outputs.map(output => addIdToOutput(output)) })) } }); + } + + replaceCellOutput(uri: URI, index: number, outputs: vscode.CellOutput[], metadata?: vscode.WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.Output, index, outputs: outputs.map(output => addIdToOutput(output)) } }); + } + + replaceCellMetadata(uri: URI, index: number, cellMetadata: vscode.NotebookCellMetadata, metadata?: vscode.WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.Metadata, index, metadata: cellMetadata } }); + } + + // --- text + replace(uri: URI, range: Range, newText: string, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: 2, uri, edit: new TextEdit(range, newText), metadata }); + this._edits.push({ _type: FileEditType.Text, uri, edit: new TextEdit(range, newText), metadata }); } insert(resource: URI, position: Position, newText: string, metadata?: vscode.WorkspaceEditEntryMetadata): void { @@ -619,8 +657,10 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { this.replace(resource, range, '', metadata); } + // --- text (Maplike) + has(uri: URI): boolean { - return this._edits.some(edit => edit._type === 2 && edit.uri.toString() === uri.toString()); + return this._edits.some(edit => edit._type === FileEditType.Text && edit.uri.toString() === uri.toString()); } set(uri: URI, edits: TextEdit[]): void { @@ -628,16 +668,16 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { // remove all text edits for `uri` for (let i = 0; i < this._edits.length; i++) { const element = this._edits[i]; - if (element._type === 2 && element.uri.toString() === uri.toString()) { + if (element._type === FileEditType.Text && element.uri.toString() === uri.toString()) { this._edits[i] = undefined!; // will be coalesced down below } } - this._edits = coalesce(this._edits); + coalesceInPlace(this._edits); } else { // append edit to the end for (const edit of edits) { if (edit) { - this._edits.push({ _type: 2, uri, edit }); + this._edits.push({ _type: FileEditType.Text, uri, edit }); } } } @@ -646,7 +686,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { get(uri: URI): TextEdit[] { const res: TextEdit[] = []; for (let candidate of this._edits) { - if (candidate._type === 2 && candidate.uri.toString() === uri.toString()) { + if (candidate._type === FileEditType.Text && candidate.uri.toString() === uri.toString()) { res.push(candidate.edit); } } @@ -654,13 +694,13 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { } entries(): [URI, TextEdit[]][] { - const textEdits = new Map(); + const textEdits = new ResourceMap<[URI, TextEdit[]]>(); for (let candidate of this._edits) { - if (candidate._type === 2) { - let textEdit = textEdits.get(candidate.uri.toString()); + if (candidate._type === FileEditType.Text) { + let textEdit = textEdits.get(candidate.uri); if (!textEdit) { textEdit = [candidate.uri, []]; - textEdits.set(candidate.uri.toString(), textEdit); + textEdits.set(candidate.uri, textEdit); } textEdit[1].push(candidate.edit); } @@ -668,22 +708,6 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { return [...textEdits.values()]; } - allEntries(): ReadonlyArray { - return this._edits; - } - - // _allEntries(): ([URI, TextEdit] | [URI?, URI?, IFileOperationOptions?])[] { - // const res: ([URI, TextEdit] | [URI?, URI?, IFileOperationOptions?])[] = []; - // for (let edit of this._edits) { - // if (edit._type === 1) { - // res.push([edit.from, edit.to, edit.options]); - // } else { - // res.push([edit.uri, edit.edit]); - // } - // } - // return res; - // } - get size(): number { return this.entries().length; } @@ -885,6 +909,12 @@ export class Diagnostic { tags?: DiagnosticTag[]; constructor(range: Range, message: string, severity: DiagnosticSeverity = DiagnosticSeverity.Error) { + if (!Range.isRange(range)) { + throw new TypeError('range must be set'); + } + if (!message) { + throw new TypeError('message must be set'); + } this.range = range; this.message = message; this.severity = severity; @@ -1825,26 +1855,26 @@ export enum TaskScope { Workspace = 2 } -export class CustomExecution implements vscode.CustomExecution2 { - private _callback: (resolvedDefintion?: vscode.TaskDefinition) => Thenable; - constructor(callback: (resolvedDefintion?: vscode.TaskDefinition) => Thenable) { +export class CustomExecution implements vscode.CustomExecution { + private _callback: (resolvedDefintion: vscode.TaskDefinition) => Thenable; + constructor(callback: (resolvedDefintion: vscode.TaskDefinition) => Thenable) { this._callback = callback; } public computeId(): string { return 'customExecution' + generateUuid(); } - public set callback(value: (resolvedDefintion?: vscode.TaskDefinition) => Thenable) { + public set callback(value: (resolvedDefintion: vscode.TaskDefinition) => Thenable) { this._callback = value; } - public get callback(): ((resolvedDefintion?: vscode.TaskDefinition) => Thenable) { + public get callback(): ((resolvedDefintion: vscode.TaskDefinition) => Thenable) { return this._callback; } } @es5ClassCompat -export class Task implements vscode.Task2 { +export class Task implements vscode.Task { private static ExtensionCallbackType: string = 'customExecution'; private static ProcessType: string = 'process'; @@ -2288,6 +2318,12 @@ export class DebugAdapterServer implements vscode.DebugAdapterServer { } } +@es5ClassCompat +export class DebugAdapterNamedPipeServer implements vscode.DebugAdapterNamedPipeServer { + constructor(public readonly path: string) { + } +} + @es5ClassCompat export class DebugAdapterInlineImplementation implements vscode.DebugAdapterInlineImplementation { readonly implementation: vscode.DebugAdapter; @@ -2744,6 +2780,18 @@ export enum NotebookRunState { Idle = 2 } +export enum NotebookCellStatusBarAlignment { + Left = 1, + Right = 2 +} + +export enum NotebookEditorRevealType { + Default = 0, + InCenter = 1, + InCenterIfOutsideViewport = 2 +} + + //#endregion //#region Timeline @@ -2777,6 +2825,17 @@ export enum ExtensionMode { Test = 3, } +export enum ExtensionRuntime { + /** + * The extension is running in a NodeJS extension host. Runtime access to NodeJS APIs is available. + */ + Node = 1, + /** + * The extension is running in a Webworker extension host. Runtime access is limited to Webworker APIs. + */ + Webworker = 2 +} + //#endregion ExtensionContext export enum StandardTokenType { diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index f005de99781..e5dc3c670d5 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -3,34 +3,20 @@ * 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 { hash } from 'vs/base/common/hash'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { Schemas } from 'vs/base/common/network'; -import { joinPath } from 'vs/base/common/resources'; -import { URI, UriComponents } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; +import { URI } from 'vs/base/common/uri'; import * as modes from 'vs/editor/common/modes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; -import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; -import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; -import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import type * as vscode from 'vscode'; -import { Cache } from './cache'; import * as extHostProtocol from './extHost.protocol'; -import * as extHostTypes from './extHostTypes'; - -type IconPath = URI | { light: URI, dark: URI }; export class ExtHostWebview implements vscode.Webview { - readonly #handle: extHostProtocol.WebviewPanelHandle; + readonly #handle: extHostProtocol.WebviewHandle; readonly #proxy: extHostProtocol.MainThreadWebviewsShape; readonly #deprecationService: IExtHostApiDeprecationService; @@ -44,7 +30,7 @@ export class ExtHostWebview implements vscode.Webview { #hasCalledAsWebviewUri = false; constructor( - handle: extHostProtocol.WebviewPanelHandle, + handle: extHostProtocol.WebviewHandle, proxy: extHostProtocol.MainThreadWebviewsShape, options: vscode.WebviewOptions, initData: WebviewInitData, @@ -64,7 +50,13 @@ export class ExtHostWebview implements vscode.Webview { /* internal */ readonly _onMessageEmitter = new Emitter(); public readonly onDidReceiveMessage: Event = this._onMessageEmitter.event; + readonly #onDidDisposeEmitter = new Emitter(); + /* internal */ readonly _onDidDispose: Event = this.#onDidDisposeEmitter.event; + public dispose() { + this.#onDidDisposeEmitter.fire(); + + this.#onDidDisposeEmitter.dispose(); this._onMessageEmitter.dispose(); } @@ -119,302 +111,11 @@ export class ExtHostWebview implements vscode.Webview { } } -export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPanel { - - readonly #handle: extHostProtocol.WebviewPanelHandle; - readonly #proxy: extHostProtocol.MainThreadWebviewsShape; - readonly #viewType: string; - - readonly #webview: ExtHostWebview; - readonly #options: vscode.WebviewPanelOptions; - - #title: string; - #iconPath?: IconPath; - #viewColumn: vscode.ViewColumn | undefined = undefined; - #visible: boolean = true; - #active: boolean = true; - #isDisposed: boolean = false; - - readonly #onDidDispose = this._register(new Emitter()); - public readonly onDidDispose = this.#onDidDispose.event; - - readonly #onDidChangeViewState = this._register(new Emitter()); - public readonly onDidChangeViewState = this.#onDidChangeViewState.event; - - constructor( - handle: extHostProtocol.WebviewPanelHandle, - proxy: extHostProtocol.MainThreadWebviewsShape, - viewType: string, - title: string, - viewColumn: vscode.ViewColumn | undefined, - editorOptions: vscode.WebviewPanelOptions, - webview: ExtHostWebview - ) { - super(); - this.#handle = handle; - this.#proxy = proxy; - this.#viewType = viewType; - this.#options = editorOptions; - this.#viewColumn = viewColumn; - this.#title = title; - this.#webview = webview; - } - - public dispose() { - if (this.#isDisposed) { - return; - } - - this.#isDisposed = true; - this.#onDidDispose.fire(); - this.#proxy.$disposeWebview(this.#handle); - this.#webview.dispose(); - - super.dispose(); - } - - get webview() { - this.assertNotDisposed(); - return this.#webview; - } - - get viewType(): string { - this.assertNotDisposed(); - return this.#viewType; - } - - get title(): string { - this.assertNotDisposed(); - return this.#title; - } - - set title(value: string) { - this.assertNotDisposed(); - if (this.#title !== value) { - this.#title = value; - this.#proxy.$setTitle(this.#handle, value); - } - } - - get iconPath(): IconPath | undefined { - this.assertNotDisposed(); - return this.#iconPath; - } - - set iconPath(value: IconPath | undefined) { - this.assertNotDisposed(); - if (this.#iconPath !== value) { - this.#iconPath = value; - - this.#proxy.$setIconPath(this.#handle, URI.isUri(value) ? { light: value, dark: value } : value); - } - } - - get options() { - return this.#options; - } - - get viewColumn(): vscode.ViewColumn | undefined { - this.assertNotDisposed(); - if (typeof this.#viewColumn === 'number' && this.#viewColumn < 0) { - // We are using a symbolic view column - // Return undefined instead to indicate that the real view column is currently unknown but will be resolved. - return undefined; - } - return this.#viewColumn; - } - - public get active(): boolean { - this.assertNotDisposed(); - return this.#active; - } - - public get visible(): boolean { - this.assertNotDisposed(); - return this.#visible; - } - - _updateViewState(newState: { active: boolean; visible: boolean; viewColumn: vscode.ViewColumn; }) { - if (this.#isDisposed) { - return; - } - - if (this.active !== newState.active || this.visible !== newState.visible || this.viewColumn !== newState.viewColumn) { - this.#active = newState.active; - this.#visible = newState.visible; - this.#viewColumn = newState.viewColumn; - this.#onDidChangeViewState.fire({ webviewPanel: this }); - } - } - - public postMessage(message: any): Promise { - this.assertNotDisposed(); - return this.#proxy.$postMessage(this.#handle, message); - } - - public reveal(viewColumn?: vscode.ViewColumn, preserveFocus?: boolean): void { - this.assertNotDisposed(); - this.#proxy.$reveal(this.#handle, { - viewColumn: viewColumn ? typeConverters.ViewColumn.from(viewColumn) : undefined, - preserveFocus: !!preserveFocus - }); - } - - private assertNotDisposed() { - if (this.#isDisposed) { - throw new Error('Webview is disposed'); - } - } -} - -class CustomDocumentStoreEntry { - - private _backupCounter = 1; - - constructor( - public readonly document: vscode.CustomDocument, - private readonly _storagePath: URI | undefined, - ) { } - - private readonly _edits = new Cache('custom documents'); - - private _backup?: vscode.CustomDocumentBackup; - - addEdit(item: vscode.CustomDocumentEditEvent): number { - return this._edits.add([item]); - } - - async undo(editId: number, isDirty: boolean): Promise { - await this.getEdit(editId).undo(); - if (!isDirty) { - this.disposeBackup(); - } - } - - async redo(editId: number, isDirty: boolean): Promise { - await this.getEdit(editId).redo(); - if (!isDirty) { - this.disposeBackup(); - } - } - - disposeEdits(editIds: number[]): void { - for (const id of editIds) { - this._edits.delete(id); - } - } - - getNewBackupUri(): URI { - if (!this._storagePath) { - throw new Error('Backup requires a valid storage path'); - } - const fileName = hashPath(this.document.uri) + (this._backupCounter++); - return joinPath(this._storagePath, fileName); - } - - updateBackup(backup: vscode.CustomDocumentBackup): void { - this._backup?.delete(); - this._backup = backup; - } - - disposeBackup(): void { - this._backup?.delete(); - this._backup = undefined; - } - - private getEdit(editId: number): vscode.CustomDocumentEditEvent { - const edit = this._edits.get(editId, 0); - if (!edit) { - throw new Error('No edit found'); - } - return edit; - } -} - -class CustomDocumentStore { - private readonly _documents = new Map(); - - public get(viewType: string, resource: vscode.Uri): CustomDocumentStoreEntry | undefined { - return this._documents.get(this.key(viewType, resource)); - } - - public add(viewType: string, document: vscode.CustomDocument, storagePath: URI | undefined): CustomDocumentStoreEntry { - const key = this.key(viewType, document.uri); - if (this._documents.has(key)) { - throw new Error(`Document already exists for viewType:${viewType} resource:${document.uri}`); - } - const entry = new CustomDocumentStoreEntry(document, storagePath); - this._documents.set(key, entry); - return entry; - } - - public delete(viewType: string, document: vscode.CustomDocument) { - const key = this.key(viewType, document.uri); - this._documents.delete(key); - } - - private key(viewType: string, resource: vscode.Uri): string { - return `${viewType}@@@${resource}`; - } - -} - -const enum WebviewEditorType { - Text, - Custom -} - -type ProviderEntry = { - readonly extension: IExtensionDescription; - readonly type: WebviewEditorType.Text; - readonly provider: vscode.CustomTextEditorProvider; -} | { - readonly extension: IExtensionDescription; - readonly type: WebviewEditorType.Custom; - readonly provider: vscode.CustomReadonlyEditorProvider; -}; - -class EditorProviderStore { - private readonly _providers = new Map(); - - public addTextProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider): vscode.Disposable { - return this.add(WebviewEditorType.Text, viewType, extension, provider); - } - - public addCustomProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomReadonlyEditorProvider): vscode.Disposable { - return this.add(WebviewEditorType.Custom, viewType, extension, provider); - } - - public get(viewType: string): ProviderEntry | undefined { - return this._providers.get(viewType); - } - - private add(type: WebviewEditorType, viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider | vscode.CustomReadonlyEditorProvider): 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); - return new extHostTypes.Disposable(() => this._providers.delete(viewType)); - } -} - export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { - private static newHandle(): extHostProtocol.WebviewPanelHandle { - return generateUuid(); - } + private readonly _webviewProxy: extHostProtocol.MainThreadWebviewsShape; - private readonly _proxy: extHostProtocol.MainThreadWebviewsShape; - private readonly _webviewPanels = new Map(); - - private readonly _serializers = new Map(); - - private readonly _editorProviders = new EditorProviderStore(); - - private readonly _documents = new CustomDocumentStore(); + private readonly _webviews = new Map(); constructor( mainContext: extHostProtocol.IMainContext, @@ -422,342 +123,50 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { private readonly workspace: IExtHostWorkspace | undefined, private readonly _logService: ILogService, private readonly _deprecationService: IExtHostApiDeprecationService, - private readonly _extHostDocuments: ExtHostDocuments, - private readonly _extensionStoragePaths?: IExtensionStoragePaths, ) { - this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadWebviews); - } - - public createWebviewPanel( - extension: IExtensionDescription, - viewType: string, - title: string, - showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, - options: (vscode.WebviewPanelOptions & vscode.WebviewOptions) = {}, - ): vscode.WebviewPanel { - const viewColumn = typeof showOptions === 'object' ? showOptions.viewColumn : showOptions; - const webviewShowOptions = { - viewColumn: typeConverters.ViewColumn.from(viewColumn), - preserveFocus: typeof showOptions === 'object' && !!showOptions.preserveFocus - }; - - const handle = ExtHostWebviews.newHandle(); - this._proxy.$createWebviewPanel(toExtensionData(extension), handle, viewType, title, webviewShowOptions, convertWebviewOptions(extension, this.workspace, options)); - - const webview = new ExtHostWebview(handle, this._proxy, options, this.initData, this.workspace, extension, this._deprecationService); - const panel = new ExtHostWebviewEditor(handle, this._proxy, viewType, title, viewColumn, options, webview); - this._webviewPanels.set(handle, panel); - return panel; - } - - public registerWebviewPanelSerializer( - extension: IExtensionDescription, - viewType: string, - serializer: vscode.WebviewPanelSerializer - ): vscode.Disposable { - if (this._serializers.has(viewType)) { - throw new Error(`Serializer for '${viewType}' already registered`); - } - - this._serializers.set(viewType, { serializer, extension }); - this._proxy.$registerSerializer(viewType); - - return new extHostTypes.Disposable(() => { - this._serializers.delete(viewType); - this._proxy.$unregisterSerializer(viewType); - }); - } - - public registerCustomEditorProvider( - extension: IExtensionDescription, - viewType: string, - provider: vscode.CustomReadonlyEditorProvider | vscode.CustomTextEditorProvider, - options: { webviewOptions?: vscode.WebviewPanelOptions, supportsMultipleEditorsPerDocument?: boolean }, - ): vscode.Disposable { - const disposables = new DisposableStore(); - if ('resolveCustomTextEditor' in provider) { - disposables.add(this._editorProviders.addTextProvider(viewType, extension, provider)); - this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, { - supportsMove: !!provider.moveCustomTextEditor, - }); - } else { - disposables.add(this._editorProviders.addCustomProvider(viewType, extension, provider)); - - if (this.supportEditing(provider)) { - disposables.add(provider.onDidChangeCustomDocument(e => { - const entry = this.getCustomDocumentEntry(viewType, e.document.uri); - if (isEditEvent(e)) { - const editId = entry.addEdit(e); - this._proxy.$onDidEdit(e.document.uri, viewType, editId, e.label); - } else { - this._proxy.$onContentChange(e.document.uri, viewType); - } - })); - } - - this._proxy.$registerCustomEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, !!options.supportsMultipleEditorsPerDocument); - } - - return extHostTypes.Disposable.from( - disposables, - new extHostTypes.Disposable(() => { - this._proxy.$unregisterEditorProvider(viewType); - })); + this._webviewProxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadWebviews); } public $onMessage( - handle: extHostProtocol.WebviewPanelHandle, + handle: extHostProtocol.WebviewHandle, message: any ): void { - const panel = this.getWebviewPanel(handle); - if (panel) { - panel.webview._onMessageEmitter.fire(message); + const webview = this.getWebview(handle); + if (webview) { + webview._onMessageEmitter.fire(message); } } public $onMissingCsp( - _handle: extHostProtocol.WebviewPanelHandle, + _handle: extHostProtocol.WebviewHandle, extensionId: string ): void { this._logService.warn(`${extensionId} created a webview without a content security policy: https://aka.ms/vscode-webview-missing-csp`); } - public $onDidChangeWebviewPanelViewStates(newStates: extHostProtocol.WebviewPanelViewStateData): void { - const handles = Object.keys(newStates); - // Notify webviews of state changes in the following order: - // - Non-visible - // - Visible - // - Active - handles.sort((a, b) => { - const stateA = newStates[a]; - const stateB = newStates[b]; - if (stateA.active) { - return 1; - } - if (stateB.active) { - return -1; - } - return (+stateA.visible) - (+stateB.visible); - }); + public createNewWebview(handle: string, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, extension: IExtensionDescription): ExtHostWebview { + const webview = new ExtHostWebview(handle, this._webviewProxy, reviveOptions(options), this.initData, this.workspace, extension, this._deprecationService); + this._webviews.set(handle, webview); - for (const handle of handles) { - const panel = this.getWebviewPanel(handle); - if (!panel) { - continue; - } + webview._onDidDispose(() => { this._webviews.delete(handle); }); - const newState = newStates[handle]; - panel._updateViewState({ - active: newState.active, - visible: newState.visible, - viewColumn: typeConverters.ViewColumn.to(newState.position), - }); - } + return webview; } - async $onDidDisposeWebviewPanel(handle: extHostProtocol.WebviewPanelHandle): Promise { - const panel = this.getWebviewPanel(handle); - if (panel) { - panel.dispose(); - this._webviewPanels.delete(handle); - } + public deleteWebview(handle: string) { + this._webviews.delete(handle); } - async $deserializeWebviewPanel( - webviewHandle: extHostProtocol.WebviewPanelHandle, - viewType: string, - title: string, - state: any, - position: EditorViewColumn, - options: modes.IWebviewOptions & modes.IWebviewPanelOptions - ): Promise { - const entry = this._serializers.get(viewType); - if (!entry) { - throw new Error(`No serializer found for '${viewType}'`); - } - const { serializer, extension } = entry; - - const webview = new ExtHostWebview(webviewHandle, this._proxy, reviveOptions(options), this.initData, this.workspace, extension, this._deprecationService); - const revivedPanel = new ExtHostWebviewEditor(webviewHandle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview); - this._webviewPanels.set(webviewHandle, revivedPanel); - await serializer.deserializeWebviewPanel(revivedPanel, state); - } - - async $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, cancellation: CancellationToken) { - const entry = this._editorProviders.get(viewType); - if (!entry) { - throw new Error(`No provider found for '${viewType}'`); - } - - if (entry.type !== WebviewEditorType.Custom) { - throw new Error(`Invalid provide type for '${viewType}'`); - } - - const revivedResource = URI.revive(resource); - const document = await entry.provider.openCustomDocument(revivedResource, { backupId }, cancellation); - - let storageRoot: URI | undefined; - if (this.supportEditing(entry.provider) && this._extensionStoragePaths) { - storageRoot = this._extensionStoragePaths.workspaceValue(entry.extension) ?? this._extensionStoragePaths.globalValue(entry.extension); - } - this._documents.add(viewType, document, storageRoot); - - return { editable: this.supportEditing(entry.provider) }; - } - - async $disposeCustomDocument(resource: UriComponents, viewType: string): Promise { - const entry = this._editorProviders.get(viewType); - if (!entry) { - throw new Error(`No provider found for '${viewType}'`); - } - - if (entry.type !== WebviewEditorType.Custom) { - throw new Error(`Invalid provider type for '${viewType}'`); - } - - const revivedResource = URI.revive(resource); - const { document } = this.getCustomDocumentEntry(viewType, revivedResource); - this._documents.delete(viewType, document); - document.dispose(); - } - - async $resolveWebviewEditor( - resource: UriComponents, - handle: extHostProtocol.WebviewPanelHandle, - viewType: string, - title: string, - position: EditorViewColumn, - options: modes.IWebviewOptions & modes.IWebviewPanelOptions, - cancellation: CancellationToken, - ): Promise { - const entry = this._editorProviders.get(viewType); - if (!entry) { - throw new Error(`No provider found for '${viewType}'`); - } - - const webview = new ExtHostWebview(handle, this._proxy, reviveOptions(options), this.initData, this.workspace, entry.extension, this._deprecationService); - const revivedPanel = new ExtHostWebviewEditor(handle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview); - this._webviewPanels.set(handle, revivedPanel); - - const revivedResource = URI.revive(resource); - - switch (entry.type) { - case WebviewEditorType.Custom: - { - const { document } = this.getCustomDocumentEntry(viewType, revivedResource); - return entry.provider.resolveCustomEditor(document, revivedPanel, cancellation); - } - case WebviewEditorType.Text: - { - const document = this._extHostDocuments.getDocument(revivedResource); - return entry.provider.resolveCustomTextEditor(document, revivedPanel, cancellation); - } - default: - { - throw new Error('Unknown webview provider type'); - } - } - } - - $disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void { - const document = this.getCustomDocumentEntry(viewType, resourceComponents); - document.disposeEdits(editIds); - } - - async $onMoveCustomEditor(handle: string, newResourceComponents: UriComponents, viewType: string): Promise { - const entry = this._editorProviders.get(viewType); - if (!entry) { - throw new Error(`No provider found for '${viewType}'`); - } - - if (!(entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor) { - throw new Error(`Provider does not implement move '${viewType}'`); - } - - const webview = this.getWebviewPanel(handle); - if (!webview) { - throw new Error(`No webview found`); - } - - const resource = URI.revive(newResourceComponents); - const document = this._extHostDocuments.getDocument(resource); - await (entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor!(document, webview, CancellationToken.None); - } - - async $undo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise { - const entry = this.getCustomDocumentEntry(viewType, resourceComponents); - return entry.undo(editId, isDirty); - } - - async $redo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise { - const entry = this.getCustomDocumentEntry(viewType, resourceComponents); - return entry.redo(editId, isDirty); - } - - async $revert(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { - const entry = this.getCustomDocumentEntry(viewType, resourceComponents); - const provider = this.getCustomEditorProvider(viewType); - await provider.revertCustomDocument(entry.document, cancellation); - entry.disposeBackup(); - } - - async $onSave(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { - const entry = this.getCustomDocumentEntry(viewType, resourceComponents); - const provider = this.getCustomEditorProvider(viewType); - await provider.saveCustomDocument(entry.document, cancellation); - entry.disposeBackup(); - } - - async $onSaveAs(resourceComponents: UriComponents, viewType: string, targetResource: UriComponents, cancellation: CancellationToken): Promise { - const entry = this.getCustomDocumentEntry(viewType, resourceComponents); - const provider = this.getCustomEditorProvider(viewType); - return provider.saveCustomDocumentAs(entry.document, URI.revive(targetResource), cancellation); - } - - async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { - const entry = this.getCustomDocumentEntry(viewType, resourceComponents); - const provider = this.getCustomEditorProvider(viewType); - - const backup = await provider.backupCustomDocument(entry.document, { - destination: entry.getNewBackupUri(), - }, cancellation); - entry.updateBackup(backup); - return backup.id; - } - - private getWebviewPanel(handle: extHostProtocol.WebviewPanelHandle): ExtHostWebviewEditor | undefined { - return this._webviewPanels.get(handle); - } - - private getCustomDocumentEntry(viewType: string, resource: UriComponents): CustomDocumentStoreEntry { - const entry = this._documents.get(viewType, URI.revive(resource)); - if (!entry) { - throw new Error('No custom document found'); - } - return entry; - } - - private getCustomEditorProvider(viewType: string): vscode.CustomEditorProvider { - const entry = this._editorProviders.get(viewType); - const provider = entry?.provider; - if (!provider || !this.supportEditing(provider)) { - throw new Error('Custom document is not editable'); - } - return provider; - } - - private supportEditing( - provider: vscode.CustomTextEditorProvider | vscode.CustomEditorProvider | vscode.CustomReadonlyEditorProvider - ): provider is vscode.CustomEditorProvider { - return !!(provider as vscode.CustomEditorProvider).onDidChangeCustomDocument; + private getWebview(handle: extHostProtocol.WebviewHandle): ExtHostWebview | undefined { + return this._webviews.get(handle); } } -function toExtensionData(extension: IExtensionDescription): extHostProtocol.WebviewExtensionDescription { +export function toExtensionData(extension: IExtensionDescription): extHostProtocol.WebviewExtensionDescription { return { id: extension.identifier, location: extension.extensionLocation }; } -function convertWebviewOptions( +export function convertWebviewOptions( extension: IExtensionDescription, workspace: IExtHostWorkspace | undefined, options: vscode.WebviewPanelOptions & vscode.WebviewOptions, @@ -786,13 +195,3 @@ function getDefaultLocalResourceRoots( extension.extensionLocation, ]; } - -function isEditEvent(e: vscode.CustomDocumentContentChangeEvent | vscode.CustomDocumentEditEvent): e is vscode.CustomDocumentEditEvent { - return typeof (e as vscode.CustomDocumentEditEvent).undo === 'function' - && typeof (e as vscode.CustomDocumentEditEvent).redo === 'function'; -} - -function hashPath(resource: URI): string { - const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString(); - return hash(str) + ''; -} diff --git a/src/vs/workbench/api/common/extHostWebviewPanels.ts b/src/vs/workbench/api/common/extHostWebviewPanels.ts new file mode 100644 index 00000000000..d29fdc631fa --- /dev/null +++ b/src/vs/workbench/api/common/extHostWebviewPanels.ts @@ -0,0 +1,299 @@ +/*--------------------------------------------------------------------------------------------- + * 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 } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import * as modes from 'vs/editor/common/modes'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; +import { convertWebviewOptions, ExtHostWebview, ExtHostWebviews, toExtensionData } from 'vs/workbench/api/common/extHostWebview'; +import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; +import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; +import type * as vscode from 'vscode'; +import * as extHostProtocol from './extHost.protocol'; +import * as extHostTypes from './extHostTypes'; + + +type IconPath = URI | { light: URI, dark: URI }; + +class ExtHostWebviewPanel extends Disposable implements vscode.WebviewPanel { + + readonly #handle: extHostProtocol.WebviewHandle; + readonly #proxy: extHostProtocol.MainThreadWebviewPanelsShape; + readonly #viewType: string; + + readonly #webview: ExtHostWebview; + readonly #options: vscode.WebviewPanelOptions; + + #title: string; + #iconPath?: IconPath; + #viewColumn: vscode.ViewColumn | undefined = undefined; + #visible: boolean = true; + #active: boolean = true; + #isDisposed: boolean = false; + + readonly #onDidDispose = this._register(new Emitter()); + public readonly onDidDispose = this.#onDidDispose.event; + + readonly #onDidChangeViewState = this._register(new Emitter()); + public readonly onDidChangeViewState = this.#onDidChangeViewState.event; + + constructor( + handle: extHostProtocol.WebviewHandle, + proxy: extHostProtocol.MainThreadWebviewPanelsShape, + viewType: string, + title: string, + viewColumn: vscode.ViewColumn | undefined, + editorOptions: vscode.WebviewPanelOptions, + webview: ExtHostWebview + ) { + super(); + this.#handle = handle; + this.#proxy = proxy; + this.#viewType = viewType; + this.#options = editorOptions; + this.#viewColumn = viewColumn; + this.#title = title; + this.#webview = webview; + } + + public dispose() { + if (this.#isDisposed) { + return; + } + + this.#isDisposed = true; + this.#onDidDispose.fire(); + + this.#proxy.$disposeWebview(this.#handle); + this.#webview.dispose(); + + super.dispose(); + } + + get webview() { + this.assertNotDisposed(); + return this.#webview; + } + + get viewType(): string { + this.assertNotDisposed(); + return this.#viewType; + } + + get title(): string { + this.assertNotDisposed(); + return this.#title; + } + + set title(value: string) { + this.assertNotDisposed(); + if (this.#title !== value) { + this.#title = value; + this.#proxy.$setTitle(this.#handle, value); + } + } + + get iconPath(): IconPath | undefined { + this.assertNotDisposed(); + return this.#iconPath; + } + + set iconPath(value: IconPath | undefined) { + this.assertNotDisposed(); + if (this.#iconPath !== value) { + this.#iconPath = value; + + this.#proxy.$setIconPath(this.#handle, URI.isUri(value) ? { light: value, dark: value } : value); + } + } + + get options() { + return this.#options; + } + + get viewColumn(): vscode.ViewColumn | undefined { + this.assertNotDisposed(); + if (typeof this.#viewColumn === 'number' && this.#viewColumn < 0) { + // We are using a symbolic view column + // Return undefined instead to indicate that the real view column is currently unknown but will be resolved. + return undefined; + } + return this.#viewColumn; + } + + public get active(): boolean { + this.assertNotDisposed(); + return this.#active; + } + + public get visible(): boolean { + this.assertNotDisposed(); + return this.#visible; + } + + _updateViewState(newState: { active: boolean; visible: boolean; viewColumn: vscode.ViewColumn; }) { + if (this.#isDisposed) { + return; + } + + if (this.active !== newState.active || this.visible !== newState.visible || this.viewColumn !== newState.viewColumn) { + this.#active = newState.active; + this.#visible = newState.visible; + this.#viewColumn = newState.viewColumn; + this.#onDidChangeViewState.fire({ webviewPanel: this }); + } + } + + public reveal(viewColumn?: vscode.ViewColumn, preserveFocus?: boolean): void { + this.assertNotDisposed(); + this.#proxy.$reveal(this.#handle, { + viewColumn: viewColumn ? typeConverters.ViewColumn.from(viewColumn) : undefined, + preserveFocus: !!preserveFocus + }); + } + + private assertNotDisposed() { + if (this.#isDisposed) { + throw new Error('Webview is disposed'); + } + } +} + +export class ExtHostWebviewPanels implements extHostProtocol.ExtHostWebviewPanelsShape { + + private static newHandle(): extHostProtocol.WebviewHandle { + return generateUuid(); + } + + private readonly _proxy: extHostProtocol.MainThreadWebviewPanelsShape; + + private readonly _webviewPanels = new Map(); + + private readonly _serializers = new Map(); + + constructor( + mainContext: extHostProtocol.IMainContext, + private readonly webviews: ExtHostWebviews, + private readonly workspace: IExtHostWorkspace | undefined, + ) { + this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadWebviewPanels); + } + + public createWebviewPanel( + extension: IExtensionDescription, + viewType: string, + title: string, + showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, + options: (vscode.WebviewPanelOptions & vscode.WebviewOptions) = {}, + ): vscode.WebviewPanel { + const viewColumn = typeof showOptions === 'object' ? showOptions.viewColumn : showOptions; + const webviewShowOptions = { + viewColumn: typeConverters.ViewColumn.from(viewColumn), + preserveFocus: typeof showOptions === 'object' && !!showOptions.preserveFocus + }; + + const handle = ExtHostWebviewPanels.newHandle(); + this._proxy.$createWebviewPanel(toExtensionData(extension), handle, viewType, title, webviewShowOptions, convertWebviewOptions(extension, this.workspace, options)); + + const webview = this.webviews.createNewWebview(handle, options, extension); + const panel = this.createNewWebviewPanel(handle, viewType, title, viewColumn, options, webview); + + return panel; + } + + public $onDidChangeWebviewPanelViewStates(newStates: extHostProtocol.WebviewPanelViewStateData): void { + const handles = Object.keys(newStates); + // Notify webviews of state changes in the following order: + // - Non-visible + // - Visible + // - Active + handles.sort((a, b) => { + const stateA = newStates[a]; + const stateB = newStates[b]; + if (stateA.active) { + return 1; + } + if (stateB.active) { + return -1; + } + return (+stateA.visible) - (+stateB.visible); + }); + + for (const handle of handles) { + const panel = this.getWebviewPanel(handle); + if (!panel) { + continue; + } + + const newState = newStates[handle]; + panel._updateViewState({ + active: newState.active, + visible: newState.visible, + viewColumn: typeConverters.ViewColumn.to(newState.position), + }); + } + } + + async $onDidDisposeWebviewPanel(handle: extHostProtocol.WebviewHandle): Promise { + const panel = this.getWebviewPanel(handle); + panel?.dispose(); + + this._webviewPanels.delete(handle); + this.webviews.deleteWebview(handle); + } + + public registerWebviewPanelSerializer( + extension: IExtensionDescription, + viewType: string, + serializer: vscode.WebviewPanelSerializer + ): vscode.Disposable { + if (this._serializers.has(viewType)) { + throw new Error(`Serializer for '${viewType}' already registered`); + } + + this._serializers.set(viewType, { serializer, extension }); + this._proxy.$registerSerializer(viewType); + + return new extHostTypes.Disposable(() => { + this._serializers.delete(viewType); + this._proxy.$unregisterSerializer(viewType); + }); + } + + async $deserializeWebviewPanel( + webviewHandle: extHostProtocol.WebviewHandle, + viewType: string, + title: string, + state: any, + position: EditorViewColumn, + options: modes.IWebviewOptions & modes.IWebviewPanelOptions + ): Promise { + const entry = this._serializers.get(viewType); + if (!entry) { + throw new Error(`No serializer found for '${viewType}'`); + } + const { serializer, extension } = entry; + + const webview = this.webviews.createNewWebview(webviewHandle, options, extension); + const revivedPanel = this.createNewWebviewPanel(webviewHandle, viewType, title, position, options, webview); + await serializer.deserializeWebviewPanel(revivedPanel, state); + } + + public createNewWebviewPanel(webviewHandle: string, viewType: string, title: string, position: number, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, webview: ExtHostWebview) { + const panel = new ExtHostWebviewPanel(webviewHandle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview); + this._webviewPanels.set(webviewHandle, panel); + return panel; + } + + public getWebviewPanel(handle: extHostProtocol.WebviewHandle): ExtHostWebviewPanel | undefined { + return this._webviewPanels.get(handle); + } +} diff --git a/src/vs/workbench/api/common/extHostWebviewView.ts b/src/vs/workbench/api/common/extHostWebviewView.ts new file mode 100644 index 00000000000..cc23d2a1fcd --- /dev/null +++ b/src/vs/workbench/api/common/extHostWebviewView.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 { CancellationToken } from 'vs/base/common/cancellation'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtHostWebview, ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; +import type * as vscode from 'vscode'; +import * as extHostProtocol from './extHost.protocol'; +import * as extHostTypes from './extHostTypes'; + +class ExtHostWebviewView extends Disposable implements vscode.WebviewView { + + readonly #handle: extHostProtocol.WebviewHandle; + readonly #proxy: extHostProtocol.MainThreadWebviewViewsShape; + + readonly #viewType: string; + readonly #webview: ExtHostWebview; + + #isDisposed = false; + #isVisible: boolean; + #title: string | undefined; + + constructor( + handle: extHostProtocol.WebviewHandle, + proxy: extHostProtocol.MainThreadWebviewViewsShape, + viewType: string, + webview: ExtHostWebview, + isVisible: boolean, + ) { + super(); + + this.#viewType = viewType; + this.#handle = handle; + this.#proxy = proxy; + this.#webview = webview; + this.#isVisible = isVisible; + } + + public dispose() { + if (this.#isDisposed) { + return; + } + + this.#isDisposed = true; + this.#onDidDispose.fire(); + + super.dispose(); + } + + readonly #onDidChangeVisibility = this._register(new Emitter()); + public readonly onDidChangeVisibility = this.#onDidChangeVisibility.event; + + readonly #onDidDispose = this._register(new Emitter()); + public readonly onDidDispose = this.#onDidDispose.event; + + public get title(): string | undefined { + this.assertNotDisposed(); + return this.#title; + } + + public set title(value: string | undefined) { + this.assertNotDisposed(); + if (this.#title !== value) { + this.#title = value; + this.#proxy.$setWebviewViewTitle(this.#handle, value); + } + } + + public get visible(): boolean { return this.#isVisible; } + + public get webview(): vscode.Webview { return this.#webview; } + + public get viewType(): string { return this.#viewType; } + + /* internal */ _setVisible(visible: boolean) { + if (visible === this.#isVisible) { + return; + } + + this.#isVisible = visible; + this.#onDidChangeVisibility.fire(); + } + + private assertNotDisposed() { + if (this.#isDisposed) { + throw new Error('Webview is disposed'); + } + } +} + +export class ExtHostWebviewViews implements extHostProtocol.ExtHostWebviewViewsShape { + + private readonly _proxy: extHostProtocol.MainThreadWebviewViewsShape; + + private readonly _viewProviders = new Map(); + + private readonly _webviewViews = new Map(); + + constructor( + mainContext: extHostProtocol.IMainContext, + private readonly _extHostWebview: ExtHostWebviews, + ) { + this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadWebviewViews); + } + + public registerWebviewViewProvider( + extension: IExtensionDescription, + viewType: string, + provider: vscode.WebviewViewProvider, + webviewOptions?: { + retainContextWhenHidden?: boolean + }, + ): vscode.Disposable { + if (this._viewProviders.has(viewType)) { + throw new Error(`View provider for '${viewType}' already registered`); + } + + this._viewProviders.set(viewType, { provider, extension }); + this._proxy.$registerWebviewViewProvider(viewType, webviewOptions); + + return new extHostTypes.Disposable(() => { + this._viewProviders.delete(viewType); + this._proxy.$unregisterWebviewViewProvider(viewType); + }); + } + + async $resolveWebviewView( + webviewHandle: string, + viewType: string, + state: any, + cancellation: CancellationToken, + ): Promise { + const entry = this._viewProviders.get(viewType); + if (!entry) { + throw new Error(`No view provider found for '${viewType}'`); + } + + const { provider, extension } = entry; + + const webview = this._extHostWebview.createNewWebview(webviewHandle, { /* todo */ }, extension); + const revivedView = new ExtHostWebviewView(webviewHandle, this._proxy, viewType, webview, true); + + this._webviewViews.set(webviewHandle, revivedView); + + await provider.resolveWebviewView(revivedView, { state }, cancellation); + } + + async $onDidChangeWebviewViewVisibility( + webviewHandle: string, + visible: boolean + ) { + const webviewView = this.getWebviewView(webviewHandle); + webviewView._setVisible(visible); + } + + async $disposeWebviewView(webviewHandle: string) { + const webviewView = this.getWebviewView(webviewHandle); + this._webviewViews.delete(webviewHandle); + webviewView.dispose(); + } + + private getWebviewView(handle: string): ExtHostWebviewView { + const entry = this._webviewViews.get(handle); + if (!entry) { + throw new Error('No webview found'); + } + return entry; + } +} diff --git a/src/vs/workbench/api/common/extHostWindow.ts b/src/vs/workbench/api/common/extHostWindow.ts index b9f1cdee4b8..a8c05964b01 100644 --- a/src/vs/workbench/api/common/extHostWindow.ts +++ b/src/vs/workbench/api/common/extHostWindow.ts @@ -12,7 +12,7 @@ import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -export class ExtHostWindow implements IExtHostWindow { +export class ExtHostWindow implements ExtHostWindowShape { private static InitialState: WindowState = { focused: true diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index 25e3d1e692d..445d2b49ba4 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -61,7 +61,12 @@ const apiMenus: IAPIMenu[] = [ { key: 'debug/callstack/context', id: MenuId.DebugCallStackContext, - description: localize('menus.debugCallstackContext', "The debug callstack context menu") + description: localize('menus.debugCallstackContext', "The debug callstack view context menu") + }, + { + key: 'debug/variables/context', + id: MenuId.DebugVariablesContext, + description: localize('menus.debugVariablesContext', "The debug variables view context menu") }, { key: 'debug/toolBar', diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 7d3cc3b33c3..4206b06776a 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import type * as vscode from 'vscode'; import * as env from 'vs/base/common/platform'; import { DebugAdapterExecutable } from 'vs/workbench/api/common/extHostTypes'; -import { ExecutableDebugAdapter, SocketDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; +import { ExecutableDebugAdapter, SocketDebugAdapter, NamedPipeDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; @@ -49,6 +49,8 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { switch (adapter.type) { case 'server': return new SocketDebugAdapter(adapter); + case 'pipeServer': + return new NamedPipeDebugAdapter(adapter); case 'executable': return new ExecutableDebugAdapter(adapter, session.type); } diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 8558835c744..3d92a699151 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -14,6 +14,7 @@ import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes'; class NodeModuleRequireInterceptor extends RequireInterceptor { @@ -43,6 +44,8 @@ class NodeModuleRequireInterceptor extends RequireInterceptor { export class ExtHostExtensionService extends AbstractExtHostExtensionService { + readonly extensionRuntime = ExtensionRuntime.Node; + protected async _beforeAlmostReadyToRunExtensions(): Promise { // initialize API and register actors const extensionApiFactory = this._instaService.invokeFunction(createApiFactoryAndRegisterActors); diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 0c59bbd7a6f..01fc86aeccf 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -11,7 +11,6 @@ import * as types from 'vs/workbench/api/common/extHostTypes'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import type * as vscode from 'vscode'; import * as tasks from '../common/shared/tasks'; -import * as Objects from 'vs/base/common/objects'; import { ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService'; import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; @@ -32,7 +31,7 @@ export class ExtHostTask extends ExtHostTaskBase { constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @IExtHostInitDataService initData: IExtHostInitDataService, - @IExtHostWorkspace workspaceService: IExtHostWorkspace, + @IExtHostWorkspace private readonly workspaceService: IExtHostWorkspace, @IExtHostDocumentsAndEditors editorService: IExtHostDocumentsAndEditors, @IExtHostConfiguration configurationService: IExtHostConfiguration, @IExtHostTerminalService extHostTerminalService: IExtHostTerminalService, @@ -55,7 +54,7 @@ export class ExtHostTask extends ExtHostTaskBase { // We have a preserved ID. So the task didn't change. if (tTask._id !== undefined) { // Always get the task execution first to prevent timing issues when retrieving it later - const handleDto = TaskHandleDTO.from(tTask); + const handleDto = TaskHandleDTO.from(tTask, this.workspaceService); const executionDTO = await this._proxy.$getTaskExecution(handleDto); if (executionDTO.task === undefined) { throw new Error('Task from execution DTO is undefined'); @@ -100,7 +99,7 @@ export class ExtHostTask extends ExtHostTaskBase { // The ID is calculated on the main thread task side, so, let's call into it here. // We need the task id's pre-computed for custom task executions because when OnDidStartTask // is invoked, we have to be able to map it back to our data. - taskIdPromises.push(this.addCustomExecution(taskDTO, task, true)); + taskIdPromises.push(this.addCustomExecution(taskDTO, task, true)); } } } @@ -123,32 +122,6 @@ export class ExtHostTask extends ExtHostTaskBase { return this._variableResolver; } - protected async resolveDefinition(uri: number | UriComponents | undefined, definition: vscode.TaskDefinition | undefined): Promise { - if (!uri || (typeof uri === 'number') || !definition) { - return definition; - } - const workspaceFolder = await this._workspaceProvider.resolveWorkspaceFolder(URI.revive(uri)); - const workspaceFolders = await this._workspaceProvider.getWorkspaceFolders2(); - if (!workspaceFolders || !workspaceFolder) { - return definition; - } - const resolver = await this.getVariableResolver(workspaceFolders); - const ws: IWorkspaceFolder = { - uri: workspaceFolder.uri, - name: workspaceFolder.name, - index: workspaceFolder.index, - toResource: () => { - throw new Error('Not implemented'); - } - }; - const resolvedDefinition = Objects.deepClone(definition); - for (const key in resolvedDefinition) { - resolvedDefinition[key] = resolver.resolve(ws, resolvedDefinition[key]); - } - - return resolvedDefinition; - } - public async $resolveVariables(uriComponents: UriComponents, toResolve: { process?: { name: string; cwd?: string; path?: string }, variables: string[] }): Promise<{ process?: string, variables: { [key: string]: string; } }> { const uri: URI = URI.revive(uriComponents); const result = { diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index c777688550a..54d99efad93 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -39,7 +39,7 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { @IExtHostDocumentsAndEditors private _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, @ILogService private _logService: ILogService ) { - super(extHostRpc); + super(true, extHostRpc); this._updateLastActiveWorkspace(); this._updateVariableResolver(); this._registerListeners(); diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts index c71ab1c7da4..870c5df9ad9 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -9,6 +9,7 @@ import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHost import { URI } from 'vs/base/common/uri'; import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes'; class WorkerRequireInterceptor extends RequireInterceptor { @@ -31,6 +32,7 @@ class WorkerRequireInterceptor extends RequireInterceptor { } export class ExtHostExtensionService extends AbstractExtHostExtensionService { + readonly extensionRuntime = ExtensionRuntime.Node; private _fakeModules?: WorkerRequireInterceptor; diff --git a/src/vs/workbench/browser/actions/developerActions.ts b/src/vs/workbench/browser/actions/developerActions.ts index 60c53ae74b4..fdce58dd22b 100644 --- a/src/vs/workbench/browser/actions/developerActions.ts +++ b/src/vs/workbench/browser/actions/developerActions.ts @@ -123,14 +123,33 @@ class ToggleScreencastModeAction extends Action2 { const onMouseUp = domEvent(container, 'mouseup', true); const onMouseMove = domEvent(container, 'mousemove', true); + const updateMouseIndicatorColor = () => { + const mouseIndicatorColor = configurationService.getValue('screencastMode.mouseIndicatorColor'); + + let style = new Option().style; + style.color = mouseIndicatorColor; + mouseMarker.style.borderColor = (mouseIndicatorColor === '' || !style.color) ? 'red' : mouseIndicatorColor; + }; + + let mouseIndicatorSize: number; + const updateMouseIndicatorSize = () => { + mouseIndicatorSize = clamp(configurationService.getValue('screencastMode.mouseIndicatorSize') || 20, 20, 100); + + mouseMarker.style.height = `${mouseIndicatorSize}px`; + mouseMarker.style.width = `${mouseIndicatorSize}px`; + }; + + updateMouseIndicatorColor(); + updateMouseIndicatorSize(); + disposables.add(onMouseDown(e => { - mouseMarker.style.top = `${e.clientY - 10}px`; - mouseMarker.style.left = `${e.clientX - 10}px`; + mouseMarker.style.top = `${e.clientY - mouseIndicatorSize / 2}px`; + mouseMarker.style.left = `${e.clientX - mouseIndicatorSize / 2}px`; mouseMarker.style.display = 'block'; const mouseMoveListener = onMouseMove(e => { - mouseMarker.style.top = `${e.clientY - 10}px`; - mouseMarker.style.left = `${e.clientX - 10}px`; + mouseMarker.style.top = `${e.clientY - mouseIndicatorSize / 2}px`; + mouseMarker.style.left = `${e.clientX - mouseIndicatorSize / 2}px`; }); Event.once(onMouseUp)(() => { @@ -150,8 +169,14 @@ class ToggleScreencastModeAction extends Action2 { keyboardMarker.style.bottom = `${clamp(configurationService.getValue('screencastMode.verticalOffset') || 0, 0, 90)}%`; }; + let keyboardMarkerTimeout: number; + const updateKeyboardMarkerTimeout = () => { + keyboardMarkerTimeout = clamp(configurationService.getValue('screencastMode.keyboardOverlayTimeout') || 800, 500, 5000); + }; + updateKeyboardFontSize(); updateKeyboardMarker(); + updateKeyboardMarkerTimeout(); disposables.add(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('screencastMode.verticalOffset')) { @@ -161,6 +186,18 @@ class ToggleScreencastModeAction extends Action2 { if (e.affectsConfiguration('screencastMode.fontSize')) { updateKeyboardFontSize(); } + + if (e.affectsConfiguration('screencastMode.keyboardOverlayTimeout')) { + updateKeyboardMarkerTimeout(); + } + + if (e.affectsConfiguration('screencastMode.mouseIndicatorColor')) { + updateMouseIndicatorColor(); + } + + if (e.affectsConfiguration('screencastMode.mouseIndicatorSize')) { + updateMouseIndicatorSize(); + } })); const onKeyDown = domEvent(window, 'keydown', true); @@ -179,7 +216,7 @@ class ToggleScreencastModeAction extends Action2 { || length > 20 || event.keyCode === KeyCode.Backspace || event.keyCode === KeyCode.Escape ) { - keyboardMarker.innerHTML = ''; + keyboardMarker.innerText = ''; length = 0; } @@ -190,7 +227,7 @@ class ToggleScreencastModeAction extends Action2 { append(keyboardMarker, key); } - const promise = timeout(800); + const promise = timeout(keyboardMarkerTimeout); keyboardTimeout = toDisposable(() => promise.cancel()); promise.then(() => { @@ -276,8 +313,27 @@ configurationRegistry.registerConfiguration({ }, 'screencastMode.onlyKeyboardShortcuts': { type: 'boolean', - description: nls.localize('screencastMode.onlyKeyboardShortcuts', "Only show keyboard shortcuts in Screencast Mode."), + description: nls.localize('screencastMode.onlyKeyboardShortcuts', "Only show keyboard shortcuts in screencast mode."), default: false - } + }, + 'screencastMode.keyboardOverlayTimeout': { + type: 'number', + default: 800, + minimum: 500, + maximum: 5000, + description: nls.localize('screencastMode.keyboardOverlayTimeout', "Controls how long (in milliseconds) the keyboard overlay is shown in screencast mode.") + }, + 'screencastMode.mouseIndicatorColor': { + type: 'string', + default: 'red', + description: nls.localize('screencastMode.mouseIndicatorColor', "Controls the color (string or Hex) of the mouse indicator in screencast mode.") + }, + 'screencastMode.mouseIndicatorSize': { + type: 'number', + default: 20, + minimum: 20, + maximum: 100, + description: nls.localize('screencastMode.mouseIndicatorSize', "Controls the size (in pixels) of the mouse indicator in screencast mode.") + }, } }); diff --git a/src/vs/workbench/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts index c190bf33cc7..5c8d8c70d1e 100644 --- a/src/vs/workbench/browser/actions/listCommands.ts +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -560,7 +560,8 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ // List if (focused instanceof List || focused instanceof PagedList) { const list = focused; - list.setSelection(range(list.length)); + const fakeKeyboardEvent = new KeyboardEvent('keydown'); + list.setSelection(range(list.length), fakeKeyboardEvent); } // Trees diff --git a/src/vs/workbench/browser/actions/media/actions.css b/src/vs/workbench/browser/actions/media/actions.css index e87f0156206..54aeeae495a 100644 --- a/src/vs/workbench/browser/actions/media/actions.css +++ b/src/vs/workbench/browser/actions/media/actions.css @@ -9,12 +9,9 @@ .monaco-workbench .screencast-mouse { position: absolute; - border: 2px solid red; - border-radius: 20px; - width: 20px; - height: 20px; - top: 0; - left: 0; + border-width: 2px; + border-style: solid; + border-radius: 50%; z-index: 100000; content: ' '; pointer-events: none; diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index 118699c1986..1de01f036f3 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -21,8 +21,6 @@ import { PanelPositionContext } from 'vs/workbench/common/panel'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -export const Deprecated_RemoteAuthorityContext = new RawContextKey('remoteAuthority', ''); - export const RemoteNameContext = new RawContextKey('remoteName', ''); export const RemoteConnectionState = new RawContextKey<'' | 'initializing' | 'disconnected' | 'connected'>('remoteConnectionState', ''); diff --git a/src/vs/workbench/browser/editor.ts b/src/vs/workbench/browser/editor.ts index 8e0aec443eb..5df3e69fd8d 100644 --- a/src/vs/workbench/browser/editor.ts +++ b/src/vs/workbench/browser/editor.ts @@ -6,17 +6,18 @@ import { EditorInput } from 'vs/workbench/common/editor'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Registry } from 'vs/platform/registry/common/platform'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IConstructorSignature0, IInstantiationService, BrandedService } from 'vs/platform/instantiation/common/instantiation'; import { insert } from 'vs/base/common/arrays'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; export interface IEditorDescriptor { - instantiate(instantiationService: IInstantiationService): BaseEditor; getId(): string; getName(): string; + instantiate(instantiationService: IInstantiationService): EditorPane; + describes(obj: unknown): boolean; } @@ -56,20 +57,20 @@ export interface IEditorRegistry { export class EditorDescriptor implements IEditorDescriptor { static create( - ctor: { new(...services: Services): BaseEditor }, + ctor: { new(...services: Services): EditorPane }, id: string, name: string ): EditorDescriptor { - return new EditorDescriptor(ctor as IConstructorSignature0, id, name); + return new EditorDescriptor(ctor as IConstructorSignature0, id, name); } constructor( - private readonly ctor: IConstructorSignature0, + private readonly ctor: IConstructorSignature0, private readonly id: string, private readonly name: string ) { } - instantiate(instantiationService: IInstantiationService): BaseEditor { + instantiate(instantiationService: IInstantiationService): EditorPane { return instantiationService.createInstance(this.ctor); } @@ -82,7 +83,7 @@ export class EditorDescriptor implements IEditorDescriptor { } describes(obj: unknown): boolean { - return obj instanceof BaseEditor && obj.getId() === this.id; + return obj instanceof EditorPane && obj.getId() === this.id; } } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 2385e99eac7..58e2817e7e0 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -1061,9 +1061,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return !this.state.activityBar.hidden; case Parts.EDITOR_PART: return !this.state.editor.hidden; + default: + return true; // any other part cannot be hidden } - - return true; // any other part cannot be hidden } focus(): void { @@ -1539,6 +1539,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi setPanelHidden(hidden: boolean, skipLayout?: boolean): void { this.state.panel.hidden = hidden; + // Return if not initialized fully #105480 + if (!this.workbenchGrid) { + return; + } + // Adjust CSS if (hidden) { addClass(this.container, Classes.PANEL_HIDDEN); @@ -1632,7 +1637,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.state.menuBar.visibility = visibility; // Layout - if (!skipLayout) { + if (!skipLayout && this.workbenchGrid) { this.workbenchGrid.setViewVisible(this.titleBarPartView, this.isVisible(Parts.TITLEBAR_PART)); } } diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index ff18883e95e..241d3bced2d 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -28,12 +28,13 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { Codicon } from 'vs/base/common/codicons'; import { isMacintosh } from 'vs/base/common/platform'; -import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { getCurrentAuthenticationSessionInfo, IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; import { AuthenticationSession } from 'vs/editor/common/modes'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IProductService } from 'vs/platform/product/common/productService'; export class ViewContainerActivityAction extends ActivityAction { @@ -125,7 +126,8 @@ export class AccountsActionViewItem extends ActivityActionViewItem { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @IStorageService private readonly storageService: IStorageService + @IStorageService private readonly storageService: IStorageService, + @IProductService private readonly productService: IProductService, ) { super(action, { draggable: false, colors, icon: true }, themeService); } @@ -178,10 +180,11 @@ export class AccountsActionViewItem extends ActivityActionViewItem { const result = await Promise.all(allSessions); let menus: IAction[] = []; + const authenticationSession = this.environmentService.options?.credentialsProvider ? await getCurrentAuthenticationSessionInfo(this.environmentService, this.productService) : undefined; result.forEach(sessionInfo => { const providerDisplayName = this.authenticationService.getLabel(sessionInfo.providerId); Object.keys(sessionInfo.sessions).forEach(accountName => { - const hasEmbedderAccountSession = sessionInfo.sessions[accountName].some(session => session.id === this.environmentService.options?.authenticationSessionId); + const hasEmbedderAccountSession = sessionInfo.sessions[accountName].some(session => session.id === (authenticationSession?.id || this.environmentService.options?.authenticationSessionId)); const manageExtensionsAction = new Action(`configureSessions${accountName}`, nls.localize('manageTrustedExtensions', "Manage Trusted Extensions"), '', true, _ => { return this.authenticationService.manageTrustedExtensionsForAccount(sessionInfo.providerId, accountName); }); diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 1d8b7d7999d..d2f8b902d61 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -168,13 +168,12 @@ export class ActivitybarPart extends Part implements IActivityBarService { const toggleAccountsVisibilityAction = new Action( 'toggleAccountsVisibility', - nls.localize('accounts', "Accounts"), + this.accountsVisibilityPreference ? nls.localize('hideAccounts', "Hide Accounts") : nls.localize('showAccounts', "Show Accounts"), undefined, true, async () => { this.accountsVisibilityPreference = !this.accountsVisibilityPreference; } ); - toggleAccountsVisibilityAction.checked = !!this.accountsActivityAction; actions.push(toggleAccountsVisibilityAction); actions.push(new Separator()); diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index ca270875dfb..bed01dd0687 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -6,8 +6,8 @@ import 'vs/css!./media/binaryeditor'; import * as nls from 'vs/nls'; import { Emitter } from 'vs/base/common/event'; -import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorInput, EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -30,7 +30,7 @@ export interface IOpenCallbacks { /* * This class is only intended to be subclassed and not instantiated. */ -export abstract class BaseBinaryResourceEditor extends BaseEditor { +export abstract class BaseBinaryResourceEditor extends EditorPane { private readonly _onMetadataChanged = this._register(new Emitter()); readonly onMetadataChanged = this._onMetadataChanged.event; @@ -74,8 +74,8 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { parent.appendChild(this.scrollbar.getDomNode()); } - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - await super.setInput(input, options, token); + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); const model = await input.resolve(); // Check for cancellation diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index eca511ebe7b..235730230bd 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -98,7 +98,7 @@ class Item extends BreadcrumbsItem { } else if (this.element instanceof OutlineModel) { // has outline element but not in one let label = document.createElement('div'); - label.innerHTML = '…'; + label.innerText = '\u2026'; label.className = 'hint-more'; container.appendChild(label); diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index e562c83198d..848df29f9ca 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -12,7 +12,7 @@ import { Event } from 'vs/base/common/event'; import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ISerializableView } from 'vs/base/browser/ui/grid/grid'; -import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { getIEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IEditorService, IResourceEditorInputType } from 'vs/workbench/services/editor/common/editorService'; @@ -132,7 +132,7 @@ export interface IEditorGroupView extends IDisposable, ISerializableView, IEdito } export function getActiveTextEditorOptions(group: IEditorGroup, expectedActiveEditor?: IEditorInput, presetOptions?: EditorOptions): EditorOptions { - const activeGroupCodeEditor = group.activeEditorPane ? getCodeEditor(group.activeEditorPane.getControl()) : undefined; + const activeGroupCodeEditor = group.activeEditorPane ? getIEditor(group.activeEditorPane.getControl()) : undefined; if (activeGroupCodeEditor) { if (!expectedActiveEditor || expectedActiveEditor.matches(group.activeEditor)) { return TextEditorOptions.fromEditor(activeGroupCodeEditor, presetOptions); diff --git a/src/vs/workbench/browser/parts/editor/editorControl.ts b/src/vs/workbench/browser/parts/editor/editorControl.ts index b3a49e5d197..5c27e74e8d2 100644 --- a/src/vs/workbench/browser/parts/editor/editorControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorControl.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { EditorInput, EditorOptions, IVisibleEditorPane } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorOpenContext, IVisibleEditorPane } from 'vs/workbench/common/editor'; import { Dimension, show, hide, addClass } from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; import { IEditorRegistry, Extensions as EditorExtensions, IEditorDescriptor } from 'vs/workbench/browser/editor'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorProgressService, LongRunningOperation } from 'vs/platform/progress/common/progress'; import { IEditorGroupView, DEFAULT_EDITOR_MIN_DIMENSIONS, DEFAULT_EDITOR_MAX_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; @@ -17,7 +17,7 @@ import { Emitter } from 'vs/base/common/event'; import { assertIsDefined } from 'vs/base/common/types'; export interface IOpenEditorResult { - readonly editorPane: BaseEditor; + readonly editorPane: EditorPane; readonly editorChanged: boolean; } @@ -34,10 +34,10 @@ export class EditorControl extends Disposable { private _onDidSizeConstraintsChange = this._register(new Emitter<{ width: number; height: number; } | undefined>()); readonly onDidSizeConstraintsChange = this._onDidSizeConstraintsChange.event; - private _activeEditorPane: BaseEditor | null = null; + private _activeEditorPane: EditorPane | null = null; get activeEditorPane(): IVisibleEditorPane | null { return this._activeEditorPane as IVisibleEditorPane | null; } - private readonly editorPanes: BaseEditor[] = []; + private readonly editorPanes: EditorPane[] = []; private readonly activeEditorPaneDisposables = this._register(new DisposableStore()); private dimension: Dimension | undefined; @@ -53,7 +53,7 @@ export class EditorControl extends Disposable { super(); } - async openEditor(editor: EditorInput, options?: EditorOptions): Promise { + async openEditor(editor: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext): Promise { // Editor pane const descriptor = Registry.as(EditorExtensions.Editors).getEditor(editor); @@ -63,11 +63,11 @@ export class EditorControl extends Disposable { const editorPane = this.doShowEditorPane(descriptor); // Set input - const editorChanged = await this.doSetInput(editorPane, editor, options); + const editorChanged = await this.doSetInput(editorPane, editor, options, context); return { editorPane, editorChanged }; } - private doShowEditorPane(descriptor: IEditorDescriptor): BaseEditor { + private doShowEditorPane(descriptor: IEditorDescriptor): EditorPane { // Return early if the currently active editor pane can handle the input if (this._activeEditorPane && descriptor.describes(this._activeEditorPane)) { @@ -99,7 +99,7 @@ export class EditorControl extends Disposable { return editorPane; } - private doCreateEditorPane(descriptor: IEditorDescriptor): BaseEditor { + private doCreateEditorPane(descriptor: IEditorDescriptor): EditorPane { // Instantiate editor const editorPane = this.doInstantiateEditorPane(descriptor); @@ -116,7 +116,7 @@ export class EditorControl extends Disposable { return editorPane; } - private doInstantiateEditorPane(descriptor: IEditorDescriptor): BaseEditor { + private doInstantiateEditorPane(descriptor: IEditorDescriptor): EditorPane { // Return early if already instantiated const existingEditorPane = this.editorPanes.find(editorPane => descriptor.describes(editorPane)); @@ -131,7 +131,7 @@ export class EditorControl extends Disposable { return editorPane; } - private doSetActiveEditorPane(editorPane: BaseEditor | null) { + private doSetActiveEditorPane(editorPane: EditorPane | null) { this._activeEditorPane = editorPane; // Clear out previous active editor pane listeners @@ -147,7 +147,7 @@ export class EditorControl extends Disposable { this._onDidSizeConstraintsChange.fire(undefined); } - private async doSetInput(editorPane: BaseEditor, editor: EditorInput, options: EditorOptions | undefined): Promise { + private async doSetInput(editorPane: EditorPane, editor: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext): Promise { // If the input did not change, return early and only apply the options // unless the options instruct us to force open it even if it is the same @@ -174,7 +174,7 @@ export class EditorControl extends Disposable { // Call into editor pane const editorWillChange = !inputMatches; try { - await editorPane.setInput(editor, options, operation.token); + await editorPane.setInput(editor, options, context, operation.token); // Focus (unless prevented or another operation is running) if (operation.isCurrent()) { diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index bc9f5db8216..d7ae2ba76a1 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -12,7 +12,7 @@ import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { IEditorIdentifier, EditorInput, EditorOptions } from 'vs/workbench/common/editor'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; -import { GroupDirection, MergeGroupMode } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { GroupDirection, MergeGroupMode, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService'; import { toDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -277,13 +277,13 @@ class DropOverlay extends Themable { pinned: true, // always pin dropped editor sticky: sourceGroup.isSticky(draggedEditor.editor) // preserve sticky state })); - targetGroup.openEditor(draggedEditor.editor, options); + const copyEditor = this.isCopyOperation(event, draggedEditor); + targetGroup.openEditor(draggedEditor.editor, options, copyEditor ? OpenEditorContext.COPY_EDITOR : OpenEditorContext.MOVE_EDITOR); // Ensure target has focus targetGroup.focus(); // Close in source group unless we copy - const copyEditor = this.isCopyOperation(event, draggedEditor); if (!copyEditor) { sourceGroup.closeEditor(draggedEditor.editor); } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 0bf99cb44ea..0fcb9999b13 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -459,7 +459,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const activeElement = document.activeElement; // Show active editor - await this.doShowEditor(activeEditor, true, options); + await this.doShowEditor(activeEditor, { active: true, isNew: false /* restored */ }, options); // Set focused now if this is the active group and focus has // not changed meanwhile. This prevents focus from being @@ -954,10 +954,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Update model and make sure to continue to use the editor we get from // the model. It is possible that the editor was already opened and we // want to ensure that we use the existing instance in that case. - const openedEditor = this._group.openEditor(editor, openEditorOptions); + const { editor: openedEditor, isNew } = this._group.openEditor(editor, openEditorOptions); // Show editor - const showEditorResult = this.doShowEditor(openedEditor, !!openEditorOptions.active, options); + const showEditorResult = this.doShowEditor(openedEditor, { active: !!openEditorOptions.active, isNew }, options); // Finally make sure the group is active or restored as instructed if (activateGroup) { @@ -969,14 +969,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return showEditorResult; } - private async doShowEditor(editor: EditorInput, active: boolean, options?: EditorOptions): Promise { + private async doShowEditor(editor: EditorInput, context: { active: boolean, isNew: boolean }, options?: EditorOptions): Promise { // Show in editor control if the active editor changed let openEditorPromise: Promise | undefined; - if (active) { + if (context.active) { openEditorPromise = (async () => { try { - const result = await this.editorControl.openEditor(editor, options); + const result = await this.editorControl.openEditor(editor, options, { newInGroup: context.isNew }); // Editor change event if (result.editorChanged) { diff --git a/src/vs/workbench/browser/parts/editor/baseEditor.ts b/src/vs/workbench/browser/parts/editor/editorPane.ts similarity index 96% rename from src/vs/workbench/browser/parts/editor/baseEditor.ts rename to src/vs/workbench/browser/parts/editor/editorPane.ts index cf48687419b..e5e48349b04 100644 --- a/src/vs/workbench/browser/parts/editor/baseEditor.ts +++ b/src/vs/workbench/browser/parts/editor/editorPane.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Composite } from 'vs/workbench/browser/composite'; -import { EditorInput, EditorOptions, IEditorPane, GroupIdentifier, IEditorMemento } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorPane, GroupIdentifier, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -41,7 +41,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; * * This class is only intended to be subclassed and not instantiated. */ -export abstract class BaseEditor extends Composite implements IEditorPane { +export abstract class EditorPane extends Composite implements IEditorPane { private static readonly EDITOR_MEMENTOS = new Map>(); @@ -91,10 +91,12 @@ export abstract class BaseEditor extends Composite implements IEditorPane { * to be different from the previous input that was set using the `input.matches()` * method. * + * The provided context gives more information around how the editor was opened. + * * The provided cancellation token should be used to test if the operation * was cancelled. */ - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this._input = input; this._options = options; } @@ -146,10 +148,10 @@ export abstract class BaseEditor extends Composite implements IEditorPane { protected getEditorMemento(editorGroupService: IEditorGroupsService, key: string, limit: number = 10): IEditorMemento { const mementoKey = `${this.getId()}${key}`; - let editorMemento = BaseEditor.EDITOR_MEMENTOS.get(mementoKey); + let editorMemento = EditorPane.EDITOR_MEMENTOS.get(mementoKey); if (!editorMemento) { editorMemento = new EditorMemento(this.getId(), key, this.getMemento(StorageScope.WORKSPACE), limit, editorGroupService); - BaseEditor.EDITOR_MEMENTOS.set(mementoKey, editorMemento); + EditorPane.EDITOR_MEMENTOS.set(mementoKey, editorMemento); } return editorMemento; @@ -158,7 +160,7 @@ export abstract class BaseEditor extends Composite implements IEditorPane { protected saveState(): void { // Save all editor memento for this editor type - BaseEditor.EDITOR_MEMENTOS.forEach(editorMemento => { + EditorPane.EDITOR_MEMENTOS.forEach(editorMemento => { if (editorMemento.id === this.getId()) { editorMemento.saveState(); } diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index ca5e8da0ea5..55ad66233a1 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -58,7 +58,7 @@ class GridWidgetView implements IView { } set gridWidget(grid: Grid | undefined) { - this.element.innerHTML = ''; + this.element.innerText = ''; if (grid) { this.element.appendChild(grid.element); diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 14dca63bc68..cc847e3c1e2 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -26,7 +26,6 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IFileService, FILES_ASSOCIATIONS_CONFIG } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IModeService, ILanguageSelection } from 'vs/editor/common/services/modeService'; -import { IModelService } from 'vs/editor/common/services/modelService'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { TabFocus } from 'vs/editor/common/config/commonEditorConfig'; @@ -43,7 +42,7 @@ import { ICodeEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { Schemas } from 'vs/base/common/network'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; -import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; +import { getIconClassesForModeId } from 'vs/editor/common/services/getIconClasses'; import { timeout } from 'vs/base/common/async'; import { INotificationHandle, INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { Event } from 'vs/base/common/event'; @@ -1042,7 +1041,6 @@ export class ChangeModeAction extends Action { actionId: string, actionLabel: string, @IModeService private readonly modeService: IModeService, - @IModelService private readonly modelService: IModelService, @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, @IQuickInputService private readonly quickInputService: IQuickInputService, @@ -1069,26 +1067,27 @@ export class ChangeModeAction extends Action { } // Compute mode + let currentLanguageId: string | undefined; let currentModeId: string | undefined; - let modeId: string | undefined; if (textModel) { - modeId = textModel.getLanguageIdentifier().language; - currentModeId = withNullAsUndefined(this.modeService.getLanguageName(modeId)); + currentModeId = textModel.getLanguageIdentifier().language; + currentLanguageId = withNullAsUndefined(this.modeService.getLanguageName(currentModeId)); } // All languages are valid picks const languages = this.modeService.getRegisteredLanguageNames(); const picks: QuickPickInput[] = languages.sort().map((lang, index) => { + const modeId = this.modeService.getModeIdForLanguageName(lang.toLowerCase()) || 'unknown'; let description: string; - if (currentModeId === lang) { - description = nls.localize('languageDescription', "({0}) - Configured Language", this.modeService.getModeIdForLanguageName(lang.toLowerCase())); + if (currentLanguageId === lang) { + description = nls.localize('languageDescription', "({0}) - Configured Language", modeId); } else { - description = nls.localize('languageDescriptionConfigured', "({0})", this.modeService.getModeIdForLanguageName(lang.toLowerCase())); + description = nls.localize('languageDescriptionConfigured', "({0})", modeId); } return { label: lang, - iconClasses: getIconClasses(this.modelService, this.modeService, this.getFakeResource(lang)), + iconClasses: getIconClassesForModeId(modeId), description }; }); @@ -1109,7 +1108,7 @@ export class ChangeModeAction extends Action { picks.unshift(galleryAction); } - configureModeSettings = { label: nls.localize('configureModeSettings', "Configure '{0}' language based settings...", currentModeId) }; + configureModeSettings = { label: nls.localize('configureModeSettings', "Configure '{0}' language based settings...", currentLanguageId) }; picks.unshift(configureModeSettings); configureModeAssociations = { label: nls.localize('configureAssociationsExt', "Configure File Association for '{0}'...", ext) }; picks.unshift(configureModeAssociations); @@ -1144,7 +1143,7 @@ export class ChangeModeAction extends Action { // User decided to configure settings for current language if (pick === configureModeSettings) { - this.preferencesService.openGlobalSettings(true, { editSetting: `[${withUndefinedAsNull(modeId)}]` }); + this.preferencesService.openGlobalSettings(true, { editSetting: `[${withUndefinedAsNull(currentModeId)}]` }); return; } @@ -1182,12 +1181,12 @@ export class ChangeModeAction extends Action { const languages = this.modeService.getRegisteredLanguageNames(); const picks: IQuickPickItem[] = languages.sort().map((lang, index) => { - const id = withNullAsUndefined(this.modeService.getModeIdForLanguageName(lang.toLowerCase())); + const id = withNullAsUndefined(this.modeService.getModeIdForLanguageName(lang.toLowerCase())) || 'unknown'; return { id, label: lang, - iconClasses: getIconClasses(this.modelService, this.modeService, this.getFakeResource(lang)), + iconClasses: getIconClassesForModeId(id), description: (id === currentAssociation) ? nls.localize('currentAssociation', "Current Association") : undefined }; }); @@ -1218,22 +1217,6 @@ export class ChangeModeAction extends Action { } }, 50 /* quick input is sensitive to being opened so soon after another */); } - - private getFakeResource(lang: string): URI | undefined { - let fakeResource: URI | undefined; - - const extensions = this.modeService.getExtensions(lang); - if (extensions?.length) { - fakeResource = URI.file(extensions[0]); - } else { - const filenames = this.modeService.getFilenames(lang); - if (filenames?.length) { - fakeResource = URI.file(filenames[0]); - } - } - - return fakeResource; - } } export interface IChangeEOLEntry extends IQuickPickItem { diff --git a/src/vs/workbench/browser/parts/editor/rangeDecorations.ts b/src/vs/workbench/browser/parts/editor/rangeDecorations.ts index 5f3a4cf23c5..d62cc3b59d5 100644 --- a/src/vs/workbench/browser/parts/editor/rangeDecorations.ts +++ b/src/vs/workbench/browser/parts/editor/rangeDecorations.ts @@ -10,7 +10,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IRange } from 'vs/editor/common/core/range'; import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, isCodeEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser'; import { TrackedRangeStickiness, IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; export interface IRangeHighlightDecoration { @@ -44,9 +44,11 @@ export class RangeHighlightDecorations extends Disposable { } highlightRange(range: IRangeHighlightDecoration, editor?: any) { - editor = editor ? editor : this.getEditor(range); + editor = editor ?? this.getEditor(range); if (isCodeEditor(editor)) { this.doHighlightRange(editor, range); + } else if (isCompositeEditor(editor) && isCodeEditor(editor.activeCodeEditor)) { + this.doHighlightRange(editor.activeCodeEditor, range); } } diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index aff171a997f..ed99e7e0567 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -5,8 +5,8 @@ import * as DOM from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; -import { EditorInput, EditorOptions, SideBySideEditorInput, IEditorControl, IEditorPane } from 'vs/workbench/common/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorInput, EditorOptions, SideBySideEditorInput, IEditorControl, IEditorPane, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -19,7 +19,7 @@ import { Event, Relay, Emitter } from 'vs/base/common/event'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { assertIsDefined } from 'vs/base/common/types'; -export class SideBySideEditor extends BaseEditor { +export class SideBySideEditor extends EditorPane { static readonly ID: string = 'workbench.editor.sidebysideEditor'; @@ -33,7 +33,7 @@ export class SideBySideEditor extends BaseEditor { private get minimumSecondaryHeight() { return this.secondaryEditorPane ? this.secondaryEditorPane.minimumHeight : 0; } private get maximumSecondaryHeight() { return this.secondaryEditorPane ? this.secondaryEditorPane.maximumHeight : Number.POSITIVE_INFINITY; } - // these setters need to exist because this extends from BaseEditor + // these setters need to exist because this extends from EditorPane set minimumWidth(value: number) { /* noop */ } set maximumWidth(value: number) { /* noop */ } set minimumHeight(value: number) { /* noop */ } @@ -44,8 +44,8 @@ export class SideBySideEditor extends BaseEditor { get minimumHeight() { return this.minimumPrimaryHeight + this.minimumSecondaryHeight; } get maximumHeight() { return this.maximumPrimaryHeight + this.maximumSecondaryHeight; } - protected primaryEditorPane?: BaseEditor; - protected secondaryEditorPane?: BaseEditor; + protected primaryEditorPane?: EditorPane; + protected secondaryEditorPane?: EditorPane; private primaryEditorContainer: HTMLElement | undefined; private secondaryEditorContainer: HTMLElement | undefined; @@ -94,11 +94,11 @@ export class SideBySideEditor extends BaseEditor { this.updateStyles(); } - async setInput(newInput: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(newInput: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const oldInput = this.input as SideBySideEditorInput; - await super.setInput(newInput, options, token); + await super.setInput(newInput, options, context, token); - return this.updateInput(oldInput, (newInput as SideBySideEditorInput), options, token); + return this.updateInput(oldInput, (newInput as SideBySideEditorInput), options, context, token); } setOptions(options: EditorOptions | undefined): void { @@ -162,13 +162,13 @@ export class SideBySideEditor extends BaseEditor { return this.secondaryEditorPane; } - private async updateInput(oldInput: SideBySideEditorInput, newInput: SideBySideEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + private async updateInput(oldInput: SideBySideEditorInput, newInput: SideBySideEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { if (!newInput.matches(oldInput)) { if (oldInput) { this.disposeEditors(); } - return this.setNewInput(newInput, options, token); + return this.setNewInput(newInput, options, context, token); } if (!this.secondaryEditorPane || !this.primaryEditorPane) { @@ -176,19 +176,19 @@ export class SideBySideEditor extends BaseEditor { } await Promise.all([ - this.secondaryEditorPane.setInput(newInput.secondary, undefined, token), - this.primaryEditorPane.setInput(newInput.primary, options, token) + this.secondaryEditorPane.setInput(newInput.secondary, undefined, context, token), + this.primaryEditorPane.setInput(newInput.primary, options, context, token) ]); } - private setNewInput(newInput: SideBySideEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + private setNewInput(newInput: SideBySideEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const secondaryEditor = this.doCreateEditor(newInput.secondary, assertIsDefined(this.secondaryEditorContainer)); const primaryEditor = this.doCreateEditor(newInput.primary, assertIsDefined(this.primaryEditorContainer)); - return this.onEditorsCreated(secondaryEditor, primaryEditor, newInput.secondary, newInput.primary, options, token); + return this.onEditorsCreated(secondaryEditor, primaryEditor, newInput.secondary, newInput.primary, options, context, token); } - private doCreateEditor(editorInput: EditorInput, container: HTMLElement): BaseEditor { + private doCreateEditor(editorInput: EditorInput, container: HTMLElement): EditorPane { const descriptor = Registry.as(EditorExtensions.Editors).getEditor(editorInput); if (!descriptor) { throw new Error('No descriptor for editor found'); @@ -201,7 +201,7 @@ export class SideBySideEditor extends BaseEditor { return editor; } - private async onEditorsCreated(secondary: BaseEditor, primary: BaseEditor, secondaryInput: EditorInput, primaryInput: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + private async onEditorsCreated(secondary: EditorPane, primary: EditorPane, secondaryInput: EditorInput, primaryInput: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this.secondaryEditorPane = secondary; this.primaryEditorPane = primary; @@ -213,8 +213,8 @@ export class SideBySideEditor extends BaseEditor { this.onDidCreateEditors.fire(undefined); await Promise.all([ - this.secondaryEditorPane.setInput(secondaryInput, undefined, token), - this.primaryEditorPane.setInput(primaryInput, options, token)] + this.secondaryEditorPane.setInput(secondaryInput, undefined, context, token), + this.primaryEditorPane.setInput(primaryInput, options, context, token)] ); } diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index e03a4159dc4..22f107c6c88 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -9,7 +9,7 @@ import { isFunction, isObject, isArray, assertIsDefined } from 'vs/base/common/t import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffEditorOptions, IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BaseTextEditor, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; -import { TextEditorOptions, EditorInput, EditorOptions, TEXT_DIFF_EDITOR_ID, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, ITextDiffEditorPane, IEditorInput } from 'vs/workbench/common/editor'; +import { TextEditorOptions, EditorInput, EditorOptions, TEXT_DIFF_EDITOR_ID, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, ITextDiffEditorPane, IEditorInput, IEditorOpenContext } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { DiffNavigator } from 'vs/editor/browser/widget/diffNavigator'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; @@ -72,7 +72,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan return this.instantiationService.createInstance(DiffEditorWidget, parent, configuration); } - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { // Dispose previous diff navigator this.diffNavigatorDisposables.clear(); @@ -81,7 +81,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan this.doSaveOrClearTextDiffEditorViewState(this.input); // Set input and resolve - await super.setInput(input, options, token); + await super.setInput(input, options, context, token); try { const resolvedModel = await input.resolve(); @@ -107,9 +107,9 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan optionsGotApplied = (options).apply(diffEditor, ScrollType.Immediate); } - // Otherwise restore View State + // Otherwise restore View State unless disabled via settings let hasPreviousViewState = false; - if (!optionsGotApplied) { + if (!optionsGotApplied && this.shouldRestoreTextEditorViewState(input, context)) { hasPreviousViewState = this.restoreTextDiffEditorViewState(input, diffEditor); } @@ -288,7 +288,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan } // Clear view state if input is disposed or we are configured to not storing any state - if (input.isDisposed() || (!this.shouldRestoreViewState && (!this.group || !this.group.isOpened(input)))) { + if (input.isDisposed() || (!this.shouldRestoreTextEditorViewState(input) && (!this.group || !this.group.isOpened(input)))) { super.clearTextEditorViewState([resource], this.group); } diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index d3e594728ed..f420982dd27 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -10,8 +10,8 @@ import { Event } from 'vs/base/common/event'; import { isObject, assertIsDefined, withNullAsUndefined, isFunction } from 'vs/base/common/types'; import { Dimension } from 'vs/base/browser/dom'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { EditorInput, EditorOptions, IEditorMemento, ITextEditorPane, TextEditorOptions, IEditorCloseEvent, IEditorInput, computeEditorAriaLabel } from 'vs/workbench/common/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorInput, EditorOptions, IEditorMemento, ITextEditorPane, TextEditorOptions, IEditorCloseEvent, IEditorInput, computeEditorAriaLabel, IEditorOpenContext, toResource, SideBySideEditor } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorViewState, IEditor, ScrollType } from 'vs/editor/common/editorCommon'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -35,7 +35,7 @@ export interface IEditorConfiguration { * The base class of editors that leverage the text editor for the editing experience. This class is only intended to * be subclassed and not instantiated. */ -export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPane { +export abstract class BaseTextEditor extends EditorPane implements ITextEditorPane { static readonly TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'textEditorViewState'; @@ -47,9 +47,6 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa private readonly groupListener = this._register(new MutableDisposable()); - private _shouldRestoreViewState: boolean | undefined; - protected get shouldRestoreViewState(): boolean | undefined { return this._shouldRestoreViewState; } - private _instantiationService: IInstantiationService; protected get instantiationService(): IInstantiationService { return this._instantiationService; } protected set instantiationService(value: IInstantiationService) { this._instantiationService = value; } @@ -69,7 +66,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa this.editorMemento = this.getEditorMemento(editorGroupService, BaseTextEditor.TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY, 100); - this._register(this.textResourceConfigurationService.onDidChangeConfiguration(e => { + this._register(this.textResourceConfigurationService.onDidChangeConfiguration(() => { const resource = this.getActiveResource(); const value = resource ? this.textResourceConfigurationService.getValue(resource) : undefined; @@ -84,13 +81,9 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa this.editorContainer?.setAttribute('aria-label', ariaLabel); this.editorControl?.updateOptions({ ariaLabel }); })); - - this.updateRestoreViewStateConfiguration(); } protected handleConfigurationChangeEvent(configuration?: IEditorConfiguration): void { - this.updateRestoreViewStateConfiguration(); - if (this.isVisible()) { this.updateEditorConfiguration(configuration); } else { @@ -98,10 +91,6 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa } } - private updateRestoreViewStateConfiguration(): void { - this._shouldRestoreViewState = this.textResourceConfigurationService.getValue(undefined, 'workbench.editor.restoreViewState') ?? true /* default */; - } - private consumePendingConfigurationChangeEvent(): void { if (this.hasPendingConfigurationChange) { this.updateEditorConfiguration(); @@ -163,8 +152,8 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa return this.instantiationService.createInstance(CodeEditorWidget, parent, configuration, {}); } - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - await super.setInput(input, options, token); + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); // Update editor options after having set the input. We do this because there can be // editor input specific options (e.g. an ARIA label depending on the input showing) @@ -238,6 +227,17 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa this.editorMemento.saveEditorState(this.group, resource, editorViewState); } + protected shouldRestoreTextEditorViewState(editor: IEditorInput, context?: IEditorOpenContext): boolean { + + // new editor: check with workbench.editor.restoreViewState setting + if (context?.newInGroup) { + return this.textResourceConfigurationService.getValue(toResource(editor, { supportSideBySide: SideBySideEditor.PRIMARY }), 'workbench.editor.restoreViewState') === false ? false : true /* restore by default */; + } + + // existing editor: always restore viewstate + return true; + } + getViewState(): IEditorViewState | undefined { const resource = this.input?.resource; if (resource) { diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index bf60f8d5e3e..a99323c2ed9 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { assertIsDefined, isFunction, withNullAsUndefined } from 'vs/base/common/types'; import { ICodeEditor, getCodeEditor, IPasteEvent } from 'vs/editor/browser/editorBrowser'; -import { TextEditorOptions, EditorInput, EditorOptions } from 'vs/workbench/common/editor'; +import { TextEditorOptions, EditorInput, EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; @@ -54,13 +54,13 @@ export class AbstractTextResourceEditor extends BaseTextEditor { return nls.localize('textEditor', "Text Editor"); } - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { // Remember view settings if input changes this.saveTextResourceEditorViewState(this.input); // Set input and resolve - await super.setInput(input, options, token); + await super.setInput(input, options, context, token); const resolvedModel = await input.resolve(); // Check for cancellation @@ -85,8 +85,8 @@ export class AbstractTextResourceEditor extends BaseTextEditor { optionsGotApplied = textOptions.apply(textEditor, ScrollType.Immediate); } - // Otherwise restore View State - if (!optionsGotApplied) { + // Otherwise restore View State unless disabled via settings + if (!optionsGotApplied && this.shouldRestoreTextEditorViewState(input, context)) { this.restoreTextResourceEditorViewState(input, textEditor); } diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index c693429a87d..96ba8faeb84 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -28,7 +28,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService'; import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillResourceDataTransfers, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; import { BreadcrumbsControl, IBreadcrumbsControlOptions } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; import { IEditorGroupsAccessor, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; @@ -163,7 +163,7 @@ export abstract class TitleControl extends Themable { const activeEditorPane = this.group.activeEditorPane; // Check Active Editor - if (activeEditorPane instanceof BaseEditor) { + if (activeEditorPane instanceof EditorPane) { const result = activeEditorPane.getActionViewItem(action); if (result) { @@ -236,7 +236,7 @@ export abstract class TitleControl extends Themable { // Editor actions require the editor control to be there, so we retrieve it via service const activeEditorPane = this.group.activeEditorPane; - if (activeEditorPane instanceof BaseEditor) { + if (activeEditorPane instanceof EditorPane) { const codeEditor = getCodeEditor(activeEditorPane.getControl()); const scopedContextKeyService = codeEditor?.invokeWithinContext(accessor => accessor.get(IContextKeyService)) || this.contextKeyService; const titleBarMenu = this.menuService.createMenu(MenuId.EditorTitle, scopedContextKeyService); diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsList.css b/src/vs/workbench/browser/parts/notifications/media/notificationsList.css index 9d01ee4526b..c6be6928d49 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsList.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsList.css @@ -115,6 +115,10 @@ text-overflow: ellipsis; } +.monaco-workbench .notifications-list-container .notification-list-item .notification-list-item-buttons-container .monaco-text-button { + display: inline-block; /* to enable ellipsis in text overflow */ +} + /** Notification: Progress */ .monaco-workbench .notifications-list-container .progress-bit { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index e41c8692e57..a935e757aad 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -221,7 +221,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl // Commands for Command Palette const category = { value: localize('notifications', "Notifications"), original: 'Notifications' }; - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: SHOW_NOTIFICATIONS_CENTER, title: { value: localize('showNotifications', "Show Notifications"), original: 'Show Notifications' }, category }, when: NotificationsCenterVisibleContext.toNegated() }); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: SHOW_NOTIFICATIONS_CENTER, title: { value: localize('showNotifications', "Show Notifications"), original: 'Show Notifications' }, category } }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: HIDE_NOTIFICATIONS_CENTER, title: { value: localize('hideNotifications', "Hide Notifications"), original: 'Hide Notifications' }, category }, when: NotificationsCenterVisibleContext }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: CLEAR_ALL_NOTIFICATIONS, title: { value: localize('clearAllNotifications', "Clear All Notifications"), original: 'Clear All Notifications' }, category } }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: FOCUS_NOTIFICATION_TOAST, title: { value: localize('focusNotificationToasts', "Focus Notification Toast"), original: 'Focus Notification Toast' }, category }, when: NotificationsToastsVisibleContext }); diff --git a/src/vs/workbench/browser/parts/views/media/paneviewlet.css b/src/vs/workbench/browser/parts/views/media/paneviewlet.css index 3adeb773148..fea8488c9c1 100644 --- a/src/vs/workbench/browser/parts/views/media/paneviewlet.css +++ b/src/vs/workbench/browser/parts/views/media/paneviewlet.css @@ -38,6 +38,26 @@ -webkit-margin-after: 0; } +.monaco-pane-view .pane > .pane-header .description { + display: block; + font-weight: normal; + margin-left: 10px; + opacity: 0.6; + overflow: hidden; + text-overflow: ellipsis; + text-transform: none; + white-space: nowrap; +} + +.monaco-pane-view .pane > .pane-header .description .codicon { + font-size: 9px; + margin-left: 2px; +} + +.monaco-pane-view .pane > .pane-header:not(.expanded) .description { + display: none; +} + .monaco-pane-view .pane.horizontal:not(.expanded) > .pane-header h3.title, .monaco-pane-view .pane.horizontal:not(.expanded) > .pane-header .description { display: none; diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 028a1594494..d718c7846ca 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -9,7 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { ColorIdentifier, activeContrastBorder, foreground } from 'vs/platform/theme/common/colorRegistry'; import { attachStyler, IColorMapping, attachButtonStyler, attachLinkStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler'; import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, SIDE_BAR_SECTION_HEADER_BORDER, PANEL_BACKGROUND, SIDE_BAR_BACKGROUND, PANEL_SECTION_HEADER_FOREGROUND, PANEL_SECTION_HEADER_BACKGROUND, PANEL_SECTION_HEADER_BORDER, PANEL_SECTION_DRAG_AND_DROP_BACKGROUND, PANEL_SECTION_BORDER } from 'vs/workbench/common/theme'; -import { append, $, trackFocus, toggleClass, EventType, isAncestor, Dimension, addDisposableListener, removeClass, addClass, createCSSRule, asCSSUrl, addClasses } from 'vs/base/browser/dom'; +import { after, append, $, trackFocus, toggleClass, EventType, isAncestor, Dimension, addDisposableListener, removeClass, addClass, createCSSRule, asCSSUrl, addClasses } from 'vs/base/browser/dom'; import { IDisposable, combinedDisposable, dispose, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { firstIndex } from 'vs/base/common/arrays'; import { IAction, Separator, IActionViewItem } from 'vs/base/common/actions'; @@ -184,6 +184,11 @@ export abstract class ViewPane extends Pane implements IView { return this._title; } + private _titleDescription: string | undefined; + public get titleDescription(): string | undefined { + return this._titleDescription; + } + private readonly menuActions: ViewMenuActions; private progressBar!: ProgressBar; private progressIndicator!: IProgressIndicator; @@ -192,6 +197,7 @@ export abstract class ViewPane extends Pane implements IView { private readonly showActionsAlways: boolean = false; private headerContainer?: HTMLElement; private titleContainer?: HTMLElement; + private titleDescriptionContainer?: HTMLElement; private iconContainer?: HTMLElement; protected twistiesContainer?: HTMLElement; @@ -216,6 +222,7 @@ export abstract class ViewPane extends Pane implements IView { this.id = options.id; this._title = options.title; + this._titleDescription = options.titleDescription; this.showActionsAlways = !!options.showActionsAlways; this.focusedViewContextKey = FocusedViewContext.bindTo(contextKeyService); @@ -293,6 +300,7 @@ export abstract class ViewPane extends Pane implements IView { actionViewItemProvider: action => this.getActionViewItem(action), ariaLabel: nls.localize('viewToolbarAriaLabel', "{0} actions", this.title), getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), + renderDropdownAsChildElement: true }); this._register(this.toolbar); @@ -359,6 +367,11 @@ export abstract class ViewPane extends Pane implements IView { const calculatedTitle = this.calculateTitle(title); this.titleContainer = append(container, $('h3.title', undefined, calculatedTitle)); + + if (this._titleDescription) { + this.setTitleDescription(this._titleDescription); + } + this.iconContainer.title = calculatedTitle; this.iconContainer.setAttribute('aria-label', calculatedTitle); } @@ -378,6 +391,22 @@ export abstract class ViewPane extends Pane implements IView { this._onDidChangeTitleArea.fire(); } + private setTitleDescription(description: string | undefined) { + if (this.titleDescriptionContainer) { + this.titleDescriptionContainer.textContent = description ?? ''; + } + else if (description && this.titleContainer) { + this.titleDescriptionContainer = after(this.titleContainer, $('span.description', undefined, description)); + } + } + + protected updateTitleDescription(description?: string | undefined): void { + this.setTitleDescription(description); + + this._titleDescription = description; + this._onDidChangeTitleArea.fire(); + } + private calculateTitle(title: string): string { const viewContainer = this.viewDescriptorService.getViewContainerByViewId(this.id)!; const model = this.viewDescriptorService.getViewContainerModel(viewContainer); @@ -506,7 +535,7 @@ export abstract class ViewPane extends Pane implements IView { if (!this.shouldShowWelcome()) { removeClass(this.bodyContainer, 'welcome'); - this.viewWelcomeContainer.innerHTML = ''; + this.viewWelcomeContainer.innerText = ''; this.scrollableElement.scanDomNode(); return; } @@ -515,14 +544,14 @@ export abstract class ViewPane extends Pane implements IView { if (contents.length === 0) { removeClass(this.bodyContainer, 'welcome'); - this.viewWelcomeContainer.innerHTML = ''; + this.viewWelcomeContainer.innerText = ''; this.scrollableElement.scanDomNode(); return; } const disposables = new DisposableStore(); addClass(this.bodyContainer, 'welcome'); - this.viewWelcomeContainer.innerHTML = ''; + this.viewWelcomeContainer.innerText = ''; let buttonIndex = 0; @@ -606,6 +635,8 @@ const enum DropDirection { RIGHT } +type BoundingRect = { top: number, left: number, bottom: number, right: number }; + class ViewPaneDropOverlay extends Themable { private static readonly OVERLAY_ID = 'monaco-workbench-pane-drop-overlay'; @@ -627,6 +658,7 @@ class ViewPaneDropOverlay extends Themable { constructor( private paneElement: HTMLElement, private orientation: Orientation | undefined, + private bounds: BoundingRect | undefined, protected location: ViewContainerLocation, protected themeService: IThemeService, ) { @@ -758,7 +790,22 @@ class ViewPaneDropOverlay extends Themable { this.doPositionOverlay({ top: '0', right: '0', width: '50%', height: '100%' }); break; default: - this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '100%' }); + // const top = this.bounds?.top || 0; + // const left = this.bounds?.bottom || 0; + + let top = '0'; + let left = '0'; + let width = '100%'; + let height = '100%'; + if (this.bounds) { + const boundingRect = this.container.getBoundingClientRect(); + top = `${this.bounds.top - boundingRect.top}px`; + left = `${this.bounds.left - boundingRect.left}px`; + height = `${this.bounds.bottom - this.bounds.top}px`; + width = `${this.bounds.right - this.bounds.left}px`; + } + + this.doPositionOverlay({ top, left, width, height }); } if ((this.orientation === Orientation.VERTICAL && paneHeight <= 25) || @@ -899,9 +946,35 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(e)))); let overlay: ViewPaneDropOverlay | undefined; + const getOverlayBounds: () => BoundingRect = () => { + const fullSize = parent.getBoundingClientRect(); + const lastPane = this.panes[this.panes.length - 1].element.getBoundingClientRect(); + const top = this.orientation === Orientation.VERTICAL ? lastPane.bottom : fullSize.top; + const left = this.orientation === Orientation.HORIZONTAL ? lastPane.right : fullSize.left; + + return { + top, + bottom: fullSize.bottom, + left, + right: fullSize.right, + }; + }; + + const inBounds = (bounds: BoundingRect, pos: { x: number, y: number }) => { + return pos.x >= bounds.left && pos.x <= bounds.right && pos.y >= bounds.top && pos.y <= bounds.bottom; + }; + + + let bounds: BoundingRect; + this._register(CompositeDragAndDropObserver.INSTANCE.registerTarget(parent, { onDragEnter: (e) => { - if (!overlay && this.panes.length === 0) { + bounds = getOverlayBounds(); + if (overlay && overlay.disposed) { + overlay = undefined; + } + + if (!overlay && inBounds(bounds, e.eventData)) { const dropData = e.dragAndDropData.getData(); if (dropData.type === 'view') { @@ -912,7 +985,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { return; } - overlay = new ViewPaneDropOverlay(parent, undefined, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService); + overlay = new ViewPaneDropOverlay(parent, undefined, bounds, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService); } if (dropData.type === 'composite' && dropData.id !== this.viewContainer.id) { @@ -920,14 +993,22 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { const viewsToMove = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors; if (!viewsToMove.some(v => !v.canMoveView) && viewsToMove.length > 0) { - overlay = new ViewPaneDropOverlay(parent, undefined, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService); + overlay = new ViewPaneDropOverlay(parent, undefined, bounds, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService); } } - } }, onDragOver: (e) => { - if (this.panes.length === 0) { + if (overlay && overlay.disposed) { + overlay = undefined; + } + + if (overlay && !inBounds(bounds, e.eventData)) { + overlay.dispose(); + overlay = undefined; + } + + if (inBounds(bounds, e.eventData)) { toggleDropEffect(e.eventData.dataTransfer, 'move', overlay !== undefined); } }, @@ -954,9 +1035,20 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } } + const paneCount = this.panes.length; + if (viewsToMove.length > 0) { this.viewDescriptorService.moveViewsToContainer(viewsToMove, this.viewContainer); } + + if (paneCount > 0) { + for (const view of viewsToMove) { + const paneToMove = this.panes.find(p => p.id === view.id); + if (paneToMove) { + this.movePane(paneToMove, this.panes[this.panes.length - 1]); + } + } + } } overlay?.dispose(); @@ -1366,7 +1458,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { return; } - overlay = new ViewPaneDropOverlay(pane.dropTargetElement, this.orientation ?? Orientation.VERTICAL, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService); + overlay = new ViewPaneDropOverlay(pane.dropTargetElement, this.orientation ?? Orientation.VERTICAL, undefined, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService); } if (dropData.type === 'composite' && dropData.id !== this.viewContainer.id && !this.viewContainer.rejectAddedViews) { @@ -1374,7 +1466,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { const viewsToMove = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors; if (!viewsToMove.some(v => !v.canMoveView) && viewsToMove.length > 0) { - overlay = new ViewPaneDropOverlay(pane.dropTargetElement, this.orientation ?? Orientation.VERTICAL, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService); + overlay = new ViewPaneDropOverlay(pane.dropTargetElement, this.orientation ?? Orientation.VERTICAL, undefined, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService); } } } diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 0462617196b..55f0f189714 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -50,6 +50,9 @@ import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFil import { WebResourceIdentityService, IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IndexedDB, INDEXEDDB_LOGS_OBJECT_STORE, INDEXEDDB_USERDATA_OBJECT_STORE } from 'vs/platform/files/browser/indexedDBFileSystemProvider'; +import { BrowserRequestService } from 'vs/workbench/services/request/browser/requestService'; +import { IRequestService } from 'vs/platform/request/common/request'; +import { IUserDataInitializationService, UserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; class BrowserMain extends Disposable { @@ -180,7 +183,7 @@ class BrowserMain extends Disposable { await this.registerFileSystemProviders(environmentService, fileService, remoteAgentService, logService, logsPath); // Long running services (workspace, config, storage) - const services = await Promise.all([ + const [configurationService, storageService] = await Promise.all([ this.createWorkspaceService(payload, environmentService, fileService, remoteAgentService, logService).then(service => { // Workspace @@ -201,7 +204,16 @@ class BrowserMain extends Disposable { }) ]); - return { serviceCollection, logService, storageService: services[1] }; + // Request Service + const requestService = new BrowserRequestService(remoteAgentService, configurationService, logService); + serviceCollection.set(IRequestService, requestService); + + // initialize user data + const userDataInitializationService = new UserDataInitializationService(environmentService, fileService, storageService, productService, requestService, logService); + serviceCollection.set(IUserDataInitializationService, userDataInitializationService); + await userDataInitializationService.initializeRequiredResources(); + + return { serviceCollection, logService, storageService }; } private async registerFileSystemProviders(environmentService: IWorkbenchEnvironmentService, fileService: IFileService, remoteAgentService: IRemoteAgentService, logService: BufferLogService, logsPath: URI): Promise { diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index c482eae8ef3..7656a699f3c 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -150,8 +150,9 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio }, 'workbench.editor.restoreViewState': { 'type': 'boolean', - 'description': nls.localize('restoreViewState', "Restores the last view state (e.g. scroll position) when re-opening files after they have been closed."), + 'description': nls.localize('restoreViewState', "Restores the last view state (e.g. scroll position) when re-opening textual editors after they have been closed."), 'default': true, + 'scope': ConfigurationScope.LANGUAGE_OVERRIDABLE }, 'workbench.editor.centeredLayoutAutoResize': { 'type': 'boolean', @@ -309,7 +310,7 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio const base = '${dirty}${activeEditorShort}${separator}${rootName}${separator}${appName}'; if (isWeb) { - return base + '${separator}${remoteName}'; // Web: always show remote indicator + return base + '${separator}${remoteName}'; // Web: always show remote name } return base; @@ -412,7 +413,7 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio 'zenMode.hideActivityBar': { 'type': 'boolean', 'default': true, - 'description': nls.localize('zenMode.hideActivityBar', "Controls whether turning on Zen Mode also hides the activity bar at the left of the workbench.") + 'description': nls.localize('zenMode.hideActivityBar', "Controls whether turning on Zen Mode also hides the activity bar either at the left or right of the workbench.") }, 'zenMode.hideLineNumbers': { 'type': 'boolean', diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 202f03996a9..d698bbfe734 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -1137,6 +1137,22 @@ export class TextEditorOptions extends EditorOptions implements ITextEditorOptio } } +/** + * Context passed into `EditorPane#setInput` to give additional + * context information around why the editor was opened. + */ +export interface IEditorOpenContext { + + /** + * An indicator if the editor input is new for the group the editor is in. + * An editor is new for a group if it was not part of the group before and + * otherwise was already opened in the group and just became the active editor. + * + * This hint can e.g. be used to decide wether to restore view state or not. + */ + newInGroup?: boolean; +} + export interface IEditorIdentifier { groupId: GroupIdentifier; editor: IEditorInput; diff --git a/src/vs/workbench/common/editor/editorGroup.ts b/src/vs/workbench/common/editor/editorGroup.ts index 99d46391837..97c2058102e 100644 --- a/src/vs/workbench/common/editor/editorGroup.ts +++ b/src/vs/workbench/common/editor/editorGroup.ts @@ -19,38 +19,43 @@ const EditorOpenPositioning = { }; export interface EditorCloseEvent extends IEditorCloseEvent { - editor: EditorInput; + readonly editor: EditorInput; } export interface EditorIdentifier extends IEditorIdentifier { - groupId: GroupIdentifier; - editor: EditorInput; + readonly groupId: GroupIdentifier; + readonly editor: EditorInput; } export interface IEditorOpenOptions { - pinned?: boolean; + readonly pinned?: boolean; sticky?: boolean; active?: boolean; - index?: number; + readonly index?: number; +} + +export interface IEditorOpenResult { + readonly editor: EditorInput; + readonly isNew: boolean; } export interface ISerializedEditorInput { - id: string; - value: string; + readonly id: string; + readonly value: string; } export interface ISerializedEditorGroup { - id: number; - editors: ISerializedEditorInput[]; - mru: number[]; - preview?: number; + readonly id: number; + readonly editors: ISerializedEditorInput[]; + readonly mru: number[]; + readonly preview?: number; sticky?: number; } export function isSerializedEditorGroup(obj?: unknown): obj is ISerializedEditorGroup { const group = obj as ISerializedEditorGroup; - return obj && typeof obj === 'object' && Array.isArray(group.editors) && Array.isArray(group.mru); + return !!(obj && typeof obj === 'object' && Array.isArray(group.editors) && Array.isArray(group.mru)); } export class EditorGroup extends Disposable { @@ -174,7 +179,7 @@ export class EditorGroup extends Disposable { return this.preview; } - openEditor(candidate: EditorInput, options?: IEditorOpenOptions): EditorInput { + openEditor(candidate: EditorInput, options?: IEditorOpenOptions): IEditorOpenResult { const makeSticky = options?.sticky || (typeof options?.index === 'number' && this.isSticky(options.index)); const makePinned = options?.pinned || options?.sticky; const makeActive = options?.active || !this.activeEditor || (!makePinned && this.matches(this.preview, this.activeEditor)); @@ -274,7 +279,10 @@ export class EditorGroup extends Disposable { this.doSetActive(newEditor); } - return newEditor; + return { + editor: newEditor, + isNew: true + }; } // Existing editor @@ -302,7 +310,10 @@ export class EditorGroup extends Disposable { this.doStick(existingEditor, this.indexOf(existingEditor)); } - return existingEditor; + return { + editor: existingEditor, + isNew: false + }; } } diff --git a/src/vs/workbench/common/memento.ts b/src/vs/workbench/common/memento.ts index 97b12facfd9..309b0517b56 100644 --- a/src/vs/workbench/common/memento.ts +++ b/src/vs/workbench/common/memento.ts @@ -5,6 +5,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { isEmptyObject } from 'vs/base/common/types'; +import { onUnexpectedError } from 'vs/base/common/errors'; export type MementoObject = { [key: string]: any }; @@ -75,7 +76,15 @@ class ScopedMemento { private load(): MementoObject { const memento = this.storageService.get(this.id, this.scope); if (memento) { - return JSON.parse(memento); + try { + return JSON.parse(memento); + } catch (error) { + // Seeing reports from users unable to open editors + // from memento parsing exceptions. Log the contents + // to diagnose further + // https://github.com/microsoft/vscode/issues/102251 + onUnexpectedError(`[memento]: failed to parse contents: ${error} (id: ${this.id}, scope: ${this.scope}, contents: ${memento})`); + } } return {}; diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index b0712b4e90b..e50bd99f313 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -196,6 +196,8 @@ Registry.add(Extensions.ViewContainersRegistry, new ViewContainersRegistryImpl() export interface IViewDescriptor { + readonly type?: string; + readonly id: string; readonly name: string; @@ -244,6 +246,7 @@ export interface IAddedViewDescriptorRef extends IViewDescriptorRef { export interface IAddedViewDescriptorState { viewDescriptor: IViewDescriptor, collapsed?: boolean; + visible?: boolean; } export interface IViewContainerModel { diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts new file mode 100644 index 00000000000..6697646978b --- /dev/null +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { groupBy } from 'vs/base/common/arrays'; +import { compare } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; +import { ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; +import { WorkspaceEditMetadata } from 'vs/editor/common/modes'; +import { IProgress } from 'vs/platform/progress/common/progress'; +import { ICellEditOperation } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; + +export class ResourceNotebookCellEdit extends ResourceEdit { + + constructor( + readonly resource: URI, + readonly cellEdit: ICellEditOperation, + readonly versionId?: number, + readonly metadata?: WorkspaceEditMetadata + ) { + super(metadata); + } +} + +export class BulkCellEdits { + + constructor( + private readonly _progress: IProgress, + private readonly _edits: ResourceNotebookCellEdit[], + @INotebookEditorModelResolverService private readonly _notebookModelService: INotebookEditorModelResolverService, + ) { } + + async apply(): Promise { + + const editsByNotebook = groupBy(this._edits, (a, b) => compare(a.resource.toString(), b.resource.toString())); + + for (let group of editsByNotebook) { + const [first] = group; + const ref = await this._notebookModelService.resolve(first.resource); + + // check state + if (typeof first.versionId === 'number' && ref.object.notebook.versionId !== first.versionId) { + ref.dispose(); + throw new Error(`Notebook '${first.resource}' has changed in the meantime`); + } + + // apply edits + const cellEdits = group.map(edit => edit.cellEdit); + ref.object.notebook.applyEdit(ref.object.notebook.versionId, cellEdits, true); + ref.dispose(); + + this._progress.report(undefined); + } + } +} diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts similarity index 59% rename from src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts rename to src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts index ca37d322d6c..b1207b2323c 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts @@ -6,39 +6,28 @@ import { localize } from 'vs/nls'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IBulkEditOptions, IBulkEditResult, IBulkEditService, IBulkEditPreviewHandler } from 'vs/editor/browser/services/bulkEditService'; -import { WorkspaceFileEdit, WorkspaceTextEdit, WorkspaceEdit } from 'vs/editor/common/modes'; +import { IBulkEditOptions, IBulkEditResult, IBulkEditService, IBulkEditPreviewHandler, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { IProgress, IProgressStep, Progress } from 'vs/platform/progress/common/progress'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { BulkTextEdits } from 'vs/workbench/services/bulkEdit/browser/bulkTextEdits'; -import { BulkFileEdits } from 'vs/workbench/services/bulkEdit/browser/bulkFileEdits'; -import { ResourceMap } from 'vs/base/common/map'; - -type Edit = WorkspaceFileEdit | WorkspaceTextEdit; +import { BulkTextEdits } from 'vs/workbench/contrib/bulkEdit/browser/bulkTextEdits'; +import { BulkFileEdits } from 'vs/workbench/contrib/bulkEdit/browser/bulkFileEdits'; +import { BulkCellEdits, ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; class BulkEdit { - private readonly _label: string | undefined; - private readonly _edits: Edit[] = []; - private readonly _editor: ICodeEditor | undefined; - private readonly _progress: IProgress; - constructor( - label: string | undefined, - editor: ICodeEditor | undefined, - progress: IProgress | undefined, - edits: Edit[], + private readonly _label: string | undefined, + private readonly _editor: ICodeEditor | undefined, + private readonly _progress: IProgress, + private readonly _edits: ResourceEdit[], @IInstantiationService private readonly _instaService: IInstantiationService, @ILogService private readonly _logService: ILogService, ) { - this._label = label; - this._editor = editor; - this._progress = progress || Progress.None; - this._edits = edits; + } ariaMessage(): string { @@ -55,56 +44,56 @@ class BulkEdit { async perform(): Promise { - let seen = new ResourceMap(); - let total = 0; + if (this._edits.length === 0) { + return; + } - const groups: Edit[][] = []; - let group: Edit[] | undefined; - for (const edit of this._edits) { - if (!group - || (WorkspaceFileEdit.is(group[0]) && !WorkspaceFileEdit.is(edit)) - || (WorkspaceTextEdit.is(group[0]) && !WorkspaceTextEdit.is(edit)) - ) { - group = []; - groups.push(group); - } - group.push(edit); - - if (WorkspaceFileEdit.is(edit)) { - total += 1; - } else if (!seen.has(edit.resource)) { - seen.set(edit.resource, true); - total += 2; + const ranges: number[] = [1]; + for (let i = 1; i < this._edits.length; i++) { + if (Object.getPrototypeOf(this._edits[i - 1]) === Object.getPrototypeOf(this._edits[i])) { + ranges[ranges.length - 1]++; + } else { + ranges.push(1); } } - // define total work and progress callback - // for child operations - this._progress.report({ total }); - + this._progress.report({ total: this._edits.length }); const progress: IProgress = { report: _ => this._progress.report({ increment: 1 }) }; - // do it. - for (const group of groups) { - if (WorkspaceFileEdit.is(group[0])) { - await this._performFileEdits(group, progress); + + let index = 0; + for (let range of ranges) { + const group = this._edits.slice(index, index + range); + if (group[0] instanceof ResourceFileEdit) { + await this._performFileEdits(group, progress); + } else if (group[0] instanceof ResourceTextEdit) { + await this._performTextEdits(group, progress); + } else if (group[0] instanceof ResourceNotebookCellEdit) { + await this._performCellEdits(group, progress); } else { - await this._performTextEdits(group, progress); + console.log('UNKNOWN EDIT'); } + index = index + range; } } - private async _performFileEdits(edits: WorkspaceFileEdit[], progress: IProgress) { + private async _performFileEdits(edits: ResourceFileEdit[], progress: IProgress) { this._logService.debug('_performFileEdits', JSON.stringify(edits)); const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), progress, edits); await model.apply(); } - private async _performTextEdits(edits: WorkspaceTextEdit[], progress: IProgress): Promise { + private async _performTextEdits(edits: ResourceTextEdit[], progress: IProgress): Promise { this._logService.debug('_performTextEdits', JSON.stringify(edits)); const model = this._instaService.createInstance(BulkTextEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._editor, progress, edits); await model.apply(); } + + private async _performCellEdits(edits: ResourceNotebookCellEdit[], progress: IProgress): Promise { + this._logService.debug('_performCellEdits', JSON.stringify(edits)); + const model = this._instaService.createInstance(BulkCellEdits, progress, edits); + await model.apply(); + } } export class BulkEditService implements IBulkEditService { @@ -132,17 +121,16 @@ export class BulkEditService implements IBulkEditService { return Boolean(this._previewHandler); } - async apply(edit: WorkspaceEdit, options?: IBulkEditOptions): Promise { + async apply(edits: ResourceEdit[], options?: IBulkEditOptions): Promise { - if (edit.edits.length === 0) { + if (edits.length === 0) { return { ariaSummary: localize('nothing', "Made no edits") }; } - if (this._previewHandler && (options?.showPreview || edit.edits.some(value => value.metadata?.needsConfirmation))) { - edit = await this._previewHandler(edit, options); + if (this._previewHandler && (options?.showPreview || edits.some(value => value.metadata?.needsConfirmation))) { + edits = await this._previewHandler(edits, options); } - const { edits } = edit; let codeEditor = options?.editor; // try to find code editor if (!codeEditor) { @@ -156,15 +144,23 @@ export class BulkEditService implements IBulkEditService { // If the code editor is readonly still allow bulk edits to be applied #68549 codeEditor = undefined; } - const bulkEdit = this._instaService.createInstance(BulkEdit, options?.quotableLabel || options?.label, codeEditor, options?.progress, edits); - return bulkEdit.perform().then(() => { + + const bulkEdit = this._instaService.createInstance( + BulkEdit, + options?.quotableLabel || options?.label, + codeEditor, options?.progress ?? Progress.None, + edits + ); + + try { + await bulkEdit.perform(); return { ariaSummary: bulkEdit.ariaMessage() }; - }).catch(err => { + } catch (err) { // console.log('apply FAILED'); // console.log(err); this._logService.error(err); throw err; - }); + } } } diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts similarity index 93% rename from src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts rename to src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts index b5e768349cb..2a6e2bc7d54 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ -import { WorkspaceFileEdit, WorkspaceFileEditOptions } from 'vs/editor/common/modes'; +import { WorkspaceFileEditOptions } from 'vs/editor/common/modes'; import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { IProgress } from 'vs/platform/progress/common/progress'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -14,6 +14,7 @@ import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { VSBuffer } from 'vs/base/common/buffer'; +import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; interface IFileOperation { uris: URI[]; @@ -147,7 +148,7 @@ export class BulkFileEdits { constructor( private readonly _label: string, private readonly _progress: IProgress, - private readonly _edits: WorkspaceFileEdit[], + private readonly _edits: ResourceFileEdit[], @IInstantiationService private readonly _instaService: IInstantiationService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, ) { } @@ -159,15 +160,15 @@ export class BulkFileEdits { const options = edit.options || {}; let op: IFileOperation | undefined; - if (edit.newUri && edit.oldUri) { + if (edit.newResource && edit.oldResource) { // rename - op = this._instaService.createInstance(RenameOperation, edit.newUri, edit.oldUri, options); - } else if (!edit.newUri && edit.oldUri) { + op = this._instaService.createInstance(RenameOperation, edit.newResource, edit.oldResource, options); + } else if (!edit.newResource && edit.oldResource) { // delete file - op = this._instaService.createInstance(DeleteOperation, edit.oldUri, options); - } else if (edit.newUri && !edit.oldUri) { + op = this._instaService.createInstance(DeleteOperation, edit.oldResource, options); + } else if (edit.newResource && !edit.oldResource) { // create file - op = this._instaService.createInstance(CreateOperation, edit.newUri, options, undefined); + op = this._instaService.createInstance(CreateOperation, edit.newResource, options, undefined); } if (op) { const undoOp = await op.perform(); diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkTextEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts similarity index 88% rename from src/vs/workbench/services/bulkEdit/browser/bulkTextEdits.ts rename to src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts index ca9dfa7739c..1877843f01a 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkTextEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts @@ -11,7 +11,6 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; -import { WorkspaceTextEdit } from 'vs/editor/common/modes'; import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { IProgress } from 'vs/platform/progress/common/progress'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; @@ -19,6 +18,7 @@ import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { SingleModelEditStackElement, MultiModelEditStackElement } from 'vs/editor/common/model/editStack'; import { ResourceMap } from 'vs/base/common/map'; import { IModelService } from 'vs/editor/common/services/modelService'; +import { ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; type ValidationResult = { canApply: true } | { canApply: false, reason: URI }; @@ -39,31 +39,31 @@ class ModelEditTask implements IDisposable { this._modelReference.dispose(); } - addEdit(resourceEdit: WorkspaceTextEdit): void { - this._expectedModelVersionId = resourceEdit.modelVersionId; - const { edit } = resourceEdit; + addEdit(resourceEdit: ResourceTextEdit): void { + this._expectedModelVersionId = resourceEdit.versionId; + const { textEdit } = resourceEdit; - if (typeof edit.eol === 'number') { + if (typeof textEdit.eol === 'number') { // honor eol-change - this._newEol = edit.eol; + this._newEol = textEdit.eol; } - if (!edit.range && !edit.text) { + if (!textEdit.range && !textEdit.text) { // lacks both a range and the text return; } - if (Range.isEmpty(edit.range) && !edit.text) { + if (Range.isEmpty(textEdit.range) && !textEdit.text) { // no-op edit (replace empty range with empty text) return; } // create edit operation let range: Range; - if (!edit.range) { + if (!textEdit.range) { range = this.model.getFullModelRange(); } else { - range = Range.lift(edit.range); + range = Range.lift(textEdit.range); } - this._edits.push(EditOperation.replaceMove(range, edit.text)); + this._edits.push(EditOperation.replaceMove(range, textEdit.text)); } validate(): ValidationResult { @@ -116,13 +116,13 @@ class EditorEditTask extends ModelEditTask { export class BulkTextEdits { - private readonly _edits = new ResourceMap(); + private readonly _edits = new ResourceMap(); constructor( private readonly _label: string, private readonly _editor: ICodeEditor | undefined, private readonly _progress: IProgress, - edits: WorkspaceTextEdit[], + edits: ResourceTextEdit[], @IEditorWorkerService private readonly _editorWorker: IEditorWorkerService, @IModelService private readonly _modelService: IModelService, @ITextModelService private readonly _textModelResolverService: ITextModelService, @@ -143,9 +143,9 @@ export class BulkTextEdits { // First check if loaded models were not changed in the meantime for (const array of this._edits.values()) { for (let edit of array) { - if (typeof edit.modelVersionId === 'number') { + if (typeof edit.versionId === 'number') { let model = this._modelService.getModel(edit.resource); - if (model && model.getVersionId() !== edit.modelVersionId) { + if (model && model.getVersionId() !== edit.versionId) { // model changed in the meantime throw new Error(`${model.uri.toString()} has changed in the meantime`); } @@ -172,12 +172,12 @@ export class BulkTextEdits { for (const edit of value) { if (makeMinimal) { - const newEdits = await this._editorWorker.computeMoreMinimalEdits(edit.resource, [edit.edit]); + const newEdits = await this._editorWorker.computeMoreMinimalEdits(edit.resource, [edit.textEdit]); if (!newEdits) { task.addEdit(edit); } else { for (let moreMinialEdit of newEdits) { - task.addEdit({ ...edit, edit: moreMinialEdit }); + task.addEdit(new ResourceTextEdit(edit.resource, moreMinialEdit, edit.versionId, edit.metadata)); } } } else { @@ -186,7 +186,6 @@ export class BulkTextEdits { } tasks.push(task); - this._progress.report(undefined); }); promises.push(promise); } diff --git a/src/vs/workbench/services/bulkEdit/browser/conflicts.ts b/src/vs/workbench/contrib/bulkEdit/browser/conflicts.ts similarity index 80% rename from src/vs/workbench/services/bulkEdit/browser/conflicts.ts rename to src/vs/workbench/contrib/bulkEdit/browser/conflicts.ts index 341353d2adb..2a3ed128b4a 100644 --- a/src/vs/workbench/services/bulkEdit/browser/conflicts.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/conflicts.ts @@ -5,12 +5,12 @@ import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; -import { WorkspaceEdit, WorkspaceTextEdit } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ResourceMap } from 'vs/base/common/map'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; import { ITextModel } from 'vs/editor/common/model'; +import { ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; export class ConflictDetector { @@ -21,31 +21,35 @@ export class ConflictDetector { readonly onDidConflict: Event = this._onDidConflict.event; constructor( - workspaceEdit: WorkspaceEdit, + edits: ResourceEdit[], @IFileService fileService: IFileService, @IModelService modelService: IModelService, ) { const _workspaceEditResources = new ResourceMap(); - for (let edit of workspaceEdit.edits) { - if (WorkspaceTextEdit.is(edit)) { - + for (let edit of edits) { + if (edit instanceof ResourceTextEdit) { _workspaceEditResources.set(edit.resource, true); - - if (typeof edit.modelVersionId === 'number') { + if (typeof edit.versionId === 'number') { const model = modelService.getModel(edit.resource); - if (model && model.getVersionId() !== edit.modelVersionId) { + if (model && model.getVersionId() !== edit.versionId) { this._conflicts.set(edit.resource, true); this._onDidConflict.fire(this); } } - } else if (edit.newUri) { - _workspaceEditResources.set(edit.newUri, true); + } else if (edit instanceof ResourceFileEdit) { + if (edit.newResource) { + _workspaceEditResources.set(edit.newResource, true); - } else if (edit.oldUri) { - _workspaceEditResources.set(edit.oldUri, true); + } else if (edit.oldResource) { + _workspaceEditResources.set(edit.oldResource, true); + } + + } else { + //todo@jrieken + console.log('UNKNOWN EDIT TYPE'); } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts similarity index 95% rename from src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts rename to src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts index cfc08528e27..de1fcca2f2d 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts @@ -7,16 +7,15 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; -import { WorkspaceEdit } from 'vs/editor/common/modes'; -import { BulkEditPane } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPane'; +import { IBulkEditService, ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; +import { BulkEditPane } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane'; import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewsRegistry, FocusedViewContext, IViewsService } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { RawContextKey, IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { BulkEditPreviewProvider } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview'; +import { BulkEditPreviewProvider } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; @@ -105,18 +104,18 @@ class BulkEditPreviewContribution { @IBulkEditService bulkEditService: IBulkEditService, @IContextKeyService contextKeyService: IContextKeyService, ) { - bulkEditService.setPreviewHandler((edit) => this._previewEdit(edit)); + bulkEditService.setPreviewHandler(edits => this._previewEdit(edits)); this._ctxEnabled = BulkEditPreviewContribution.ctxEnabled.bindTo(contextKeyService); } - private async _previewEdit(edit: WorkspaceEdit) { + private async _previewEdit(edits: ResourceEdit[]): Promise { this._ctxEnabled.set(true); const uxState = this._activeSession?.uxState ?? new UXState(this._panelService, this._editorGroupsService); const view = await getBulkEditPane(this._viewsService); if (!view) { this._ctxEnabled.set(false); - return edit; + return edits; } // check for active preview session and let the user decide @@ -130,7 +129,7 @@ class BulkEditPreviewContribution { if (choice.choice === 0) { // this refactoring is being cancelled - return { edits: [] }; + return []; } } @@ -147,12 +146,7 @@ class BulkEditPreviewContribution { // the actual work... try { - const newEditOrUndefined = await view.setInput(edit, session.cts.token); - if (!newEditOrUndefined) { - return { edits: [] }; - } - - return newEditOrUndefined; + return await view.setInput(edits, session.cts.token); } finally { // restore UX state @@ -366,4 +360,3 @@ Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews ctorDescriptor: new SyncDescriptor(BulkEditPane), containerIcon: Codicon.lightbulb.classNames, }], container); - diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.css b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.css similarity index 100% rename from src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.css rename to src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.css diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts similarity index 96% rename from src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts rename to src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index b0c6afd8861..64b406d9e07 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -5,8 +5,7 @@ import 'vs/css!./bulkEdit'; import { WorkbenchAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService'; -import { WorkspaceEdit } from 'vs/editor/common/modes'; -import { BulkEditElement, BulkEditDelegate, TextEditElementRenderer, FileElementRenderer, BulkEditDataSource, BulkEditIdentityProvider, FileElement, TextEditElement, BulkEditAccessibilityProvider, CategoryElementRenderer, BulkEditNaviLabelProvider, CategoryElement, BulkEditSorter } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditTree'; +import { BulkEditElement, BulkEditDelegate, TextEditElementRenderer, FileElementRenderer, BulkEditDataSource, BulkEditIdentityProvider, FileElement, TextEditElement, BulkEditAccessibilityProvider, CategoryElementRenderer, BulkEditNaviLabelProvider, CategoryElement, BulkEditSorter } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree'; import { FuzzyScore } from 'vs/base/common/filters'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { registerThemingParticipant, IColorTheme, ICssStyleCollector, IThemeService } from 'vs/platform/theme/common/themeService'; @@ -14,7 +13,7 @@ import { diffInserted, diffRemoved } from 'vs/platform/theme/common/colorRegistr import { localize } from 'vs/nls'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { BulkEditPreviewProvider, BulkFileOperations, BulkFileOperationType } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview'; +import { BulkEditPreviewProvider, 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'; @@ -39,6 +38,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag 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'; const enum State { Data = 'data', @@ -66,7 +66,7 @@ export class BulkEditPane extends ViewPane { private readonly _disposables = new DisposableStore(); private readonly _sessionDisposables = new DisposableStore(); - private _currentResolve?: (edit?: WorkspaceEdit) => void; + private _currentResolve?: (edit?: ResourceEdit[]) => void; private _currentInput?: BulkFileOperations; @@ -163,7 +163,7 @@ export class BulkEditPane extends ViewPane { this.element.dataset['state'] = state; } - async setInput(edit: WorkspaceEdit, token: CancellationToken): Promise { + async setInput(edit: ResourceEdit[], token: CancellationToken): Promise { this._setState(State.Data); this._sessionDisposables.clear(); this._treeViewStates.clear(); @@ -307,11 +307,11 @@ export class BulkEditPane extends ViewPane { let fileElement: FileElement; if (e.element instanceof TextEditElement) { fileElement = e.element.parent; - options.selection = e.element.edit.textEdit.edit.range; + options.selection = e.element.edit.textEdit.textEdit.range; } else if (e.element instanceof FileElement) { fileElement = e.element; - options.selection = e.element.edit.textEdits[0]?.textEdit.edit.range; + options.selection = e.element.edit.textEdits[0]?.textEdit.textEdit.range; } else { // invalid event diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts similarity index 81% rename from src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts rename to src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts index d6d0d1510d3..4aecbf746cf 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; -import { WorkspaceEdit, WorkspaceTextEdit, WorkspaceFileEdit, WorkspaceEditMetadata } from 'vs/editor/common/modes'; +import { WorkspaceEditMetadata } from 'vs/editor/common/modes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { mergeSort, coalesceInPlace } from 'vs/base/common/arrays'; import { Range } from 'vs/editor/common/core/range'; @@ -17,10 +17,11 @@ import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiati import { IFileService } from 'vs/platform/files/common/files'; import { Emitter, Event } from 'vs/base/common/event'; import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; -import { ConflictDetector } from 'vs/workbench/services/bulkEdit/browser/conflicts'; +import { ConflictDetector } from 'vs/workbench/contrib/bulkEdit/browser/conflicts'; import { ResourceMap } from 'vs/base/common/map'; import { localize } from 'vs/nls'; import { extUri } from 'vs/base/common/resources'; +import { ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; export class CheckedStates { @@ -67,7 +68,7 @@ export class BulkTextEdit { constructor( readonly parent: BulkFileOperation, - readonly textEdit: WorkspaceTextEdit + readonly textEdit: ResourceTextEdit ) { } } @@ -82,7 +83,7 @@ export class BulkFileOperation { type: BulkFileOperationType = 0; textEdits: BulkTextEdit[] = []; - originalEdits = new Map(); + originalEdits = new Map(); newUri?: URI; constructor( @@ -90,14 +91,14 @@ export class BulkFileOperation { readonly parent: BulkFileOperations ) { } - addEdit(index: number, type: BulkFileOperationType, edit: WorkspaceTextEdit | WorkspaceFileEdit) { + addEdit(index: number, type: BulkFileOperationType, edit: ResourceTextEdit | ResourceFileEdit) { this.type |= type; this.originalEdits.set(index, edit); - if (WorkspaceTextEdit.is(edit)) { + if (edit instanceof ResourceTextEdit) { this.textEdits.push(new BulkTextEdit(this, edit)); } else if (type === BulkFileOperationType.Rename) { - this.newUri = edit.newUri; + this.newUri = edit.newResource; } } @@ -134,19 +135,19 @@ export class BulkCategory { export class BulkFileOperations { - static async create(accessor: ServicesAccessor, bulkEdit: WorkspaceEdit): Promise { + static async create(accessor: ServicesAccessor, bulkEdit: ResourceEdit[]): Promise { const result = accessor.get(IInstantiationService).createInstance(BulkFileOperations, bulkEdit); return await result._init(); } - readonly checked = new CheckedStates(); + readonly checked = new CheckedStates(); readonly fileOperations: BulkFileOperation[] = []; readonly categories: BulkCategory[] = []; readonly conflicts: ConflictDetector; constructor( - private readonly _bulkEdit: WorkspaceEdit, + private readonly _bulkEdit: ResourceEdit[], @IFileService private readonly _fileService: IFileService, @IInstantiationService instaService: IInstantiationService, ) { @@ -164,8 +165,8 @@ export class BulkFileOperations { const newToOldUri = new ResourceMap(); - for (let idx = 0; idx < this._bulkEdit.edits.length; idx++) { - const edit = this._bulkEdit.edits[idx]; + for (let idx = 0; idx < this._bulkEdit.length; idx++) { + const edit = this._bulkEdit[idx]; let uri: URI; let type: BulkFileOperationType; @@ -173,39 +174,45 @@ export class BulkFileOperations { // store inital checked state this.checked.updateChecked(edit, !edit.metadata?.needsConfirmation); - if (WorkspaceTextEdit.is(edit)) { + if (edit instanceof ResourceTextEdit) { type = BulkFileOperationType.TextEdit; uri = edit.resource; - } else if (edit.newUri && edit.oldUri) { - type = BulkFileOperationType.Rename; - uri = edit.oldUri; - if (edit.options?.overwrite === undefined && edit.options?.ignoreIfExists && await this._fileService.exists(uri)) { - // noop -> "soft" rename to something that already exists - continue; - } - // map newUri onto oldUri so that text-edit appear for - // the same file element - newToOldUri.set(edit.newUri, uri); + } else if (edit instanceof ResourceFileEdit) { + if (edit.newResource && edit.oldResource) { + type = BulkFileOperationType.Rename; + uri = edit.oldResource; + if (edit.options?.overwrite === undefined && edit.options?.ignoreIfExists && await this._fileService.exists(uri)) { + // noop -> "soft" rename to something that already exists + continue; + } + // map newResource onto oldResource so that text-edit appear for + // the same file element + newToOldUri.set(edit.newResource, uri); - } else if (edit.oldUri) { - type = BulkFileOperationType.Delete; - uri = edit.oldUri; - if (edit.options?.ignoreIfNotExists && !await this._fileService.exists(uri)) { - // noop -> "soft" delete something that doesn't exist - continue; - } + } else if (edit.oldResource) { + type = BulkFileOperationType.Delete; + uri = edit.oldResource; + if (edit.options?.ignoreIfNotExists && !await this._fileService.exists(uri)) { + // noop -> "soft" delete something that doesn't exist + continue; + } - } else if (edit.newUri) { - type = BulkFileOperationType.Create; - uri = edit.newUri; - if (edit.options?.overwrite === undefined && edit.options?.ignoreIfExists && await this._fileService.exists(uri)) { - // noop -> "soft" create something that already exists + } else if (edit.newResource) { + type = BulkFileOperationType.Create; + uri = edit.newResource; + if (edit.options?.overwrite === undefined && edit.options?.ignoreIfExists && await this._fileService.exists(uri)) { + // noop -> "soft" create something that already exists + continue; + } + + } else { + // invalid edit -> skip continue; } } else { - // invalid edit -> skip + // unsupported edit continue; } @@ -249,7 +256,7 @@ export class BulkFileOperations { if (file.type !== BulkFileOperationType.TextEdit) { let checked = true; for (const edit of file.originalEdits.values()) { - if (WorkspaceFileEdit.is(edit)) { + if (edit instanceof ResourceFileEdit) { checked = checked && this.checked.isChecked(edit); } } @@ -275,14 +282,14 @@ export class BulkFileOperations { return this; } - getWorkspaceEdit(): WorkspaceEdit { - const result: WorkspaceEdit = { edits: [] }; + getWorkspaceEdit(): ResourceEdit[] { + const result: ResourceEdit[] = []; let allAccepted = true; - for (let i = 0; i < this._bulkEdit.edits.length; i++) { - const edit = this._bulkEdit.edits[i]; + for (let i = 0; i < this._bulkEdit.length; i++) { + const edit = this._bulkEdit[i]; if (this.checked.isChecked(edit)) { - result.edits[i] = edit; + result[i] = edit; continue; } allAccepted = false; @@ -293,7 +300,7 @@ export class BulkFileOperations { } // not all edits have been accepted - coalesceInPlace(result.edits); + coalesceInPlace(result); return result; } @@ -306,9 +313,9 @@ export class BulkFileOperations { let ignoreAll = false; for (const edit of file.originalEdits.values()) { - if (WorkspaceTextEdit.is(edit)) { + if (edit instanceof ResourceTextEdit) { if (this.checked.isChecked(edit)) { - result.push(EditOperation.replaceMove(Range.lift(edit.edit.range), edit.edit.text)); + result.push(EditOperation.replaceMove(Range.lift(edit.textEdit.range), edit.textEdit.text)); } } else if (!this.checked.isChecked(edit)) { @@ -330,7 +337,7 @@ export class BulkFileOperations { return []; } - getUriOfEdit(edit: WorkspaceFileEdit | WorkspaceTextEdit): URI { + getUriOfEdit(edit: ResourceEdit): URI { for (let file of this.fileOperations) { for (const value of file.originalEdits.values()) { if (value === edit) { diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts similarity index 96% rename from src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts rename to src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts index 52a59197356..4db2acf28b7 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts @@ -14,7 +14,7 @@ import * as dom from 'vs/base/browser/dom'; import { ITextModel } from 'vs/editor/common/model'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { BulkFileOperations, BulkFileOperation, BulkFileOperationType, BulkTextEdit, BulkCategory } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview'; +import { BulkFileOperations, BulkFileOperation, BulkFileOperationType, BulkTextEdit, BulkCategory } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview'; import { FileKind } from 'vs/platform/files/common/files'; import { localize } from 'vs/nls'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -22,11 +22,11 @@ import type { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWid import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { basename } from 'vs/base/common/resources'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { WorkspaceFileEdit } from 'vs/editor/common/modes'; import { compare } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { Iterable } from 'vs/base/common/iterator'; +import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; // --- VIEW MODEL @@ -62,7 +62,7 @@ export class FileElement implements ICheckable { // multiple file edits -> reflect single state for (let edit of this.edit.originalEdits.values()) { - if (WorkspaceFileEdit.is(edit)) { + if (edit instanceof ResourceFileEdit) { checked = checked && model.checked.isChecked(edit); } } @@ -73,7 +73,7 @@ export class FileElement implements ICheckable { for (let file of category.fileOperations) { if (file.uri.toString() === this.edit.uri.toString()) { for (const edit of file.originalEdits.values()) { - if (WorkspaceFileEdit.is(edit)) { + if (edit instanceof ResourceFileEdit) { checked = checked && model.checked.isChecked(edit); } } @@ -113,7 +113,7 @@ export class FileElement implements ICheckable { for (let file of category.fileOperations) { if (file.uri.toString() === this.edit.uri.toString()) { for (const edit of file.originalEdits.values()) { - if (WorkspaceFileEdit.is(edit)) { + if (edit instanceof ResourceFileEdit) { checked = checked && model.checked.isChecked(edit); } } @@ -155,7 +155,7 @@ export class TextEditElement implements ICheckable { // make sure parent is checked when this element is checked... if (value) { for (const edit of this.parent.edit.originalEdits.values()) { - if (WorkspaceFileEdit.is(edit)) { + if (edit instanceof ResourceFileEdit) { (model).checked.updateChecked(edit, value); } } @@ -219,7 +219,7 @@ export class BulkEditDataSource implements IAsyncDataSource { - const range = Range.lift(edit.textEdit.edit.range); + const range = Range.lift(edit.textEdit.textEdit.range); //prefix-math let startTokens = textModel.getLineTokens(range.startLineNumber); @@ -241,7 +241,7 @@ export class BulkEditDataSource implements IAsyncDataSource { } if (a instanceof TextEditElement && b instanceof TextEditElement) { - return Range.compareRangesUsingStarts(a.edit.textEdit.edit.range, b.edit.textEdit.edit.range); + return Range.compareRangesUsingStarts(a.edit.textEdit.textEdit.range, b.edit.textEdit.textEdit.range); } return 0; @@ -336,13 +336,13 @@ export class BulkEditAccessibilityProvider implements IListAccessibilityProvider if (element instanceof TextEditElement) { if (element.selecting.length > 0 && element.inserting.length > 0) { // edit: replace - return localize('aria.replace', "line {0}, replacing {1} with {2}", element.edit.textEdit.edit.range.startLineNumber, element.selecting, element.inserting); + return localize('aria.replace', "line {0}, replacing {1} with {2}", element.edit.textEdit.textEdit.range.startLineNumber, element.selecting, element.inserting); } else if (element.selecting.length > 0 && element.inserting.length === 0) { // edit: delete - return localize('aria.del', "line {0}, removing {1}", element.edit.textEdit.edit.range.startLineNumber, element.selecting); + return localize('aria.del', "line {0}, removing {1}", element.edit.textEdit.textEdit.range.startLineNumber, element.selecting); } else if (element.selecting.length === 0 && element.inserting.length > 0) { // edit: insert - return localize('aria.insert', "line {0}, inserting {1}", element.edit.textEdit.edit.range.startLineNumber, element.selecting); + return localize('aria.insert', "line {0}, inserting {1}", element.edit.textEdit.textEdit.range.startLineNumber, element.selecting); } } diff --git a/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts b/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts index 768bb672eaa..0aac694ea89 100644 --- a/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts +++ b/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts @@ -11,10 +11,10 @@ import { InstantiationService } from 'vs/platform/instantiation/common/instantia import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IModelService } from 'vs/editor/common/services/modelService'; -import type { WorkspaceEdit } from 'vs/editor/common/modes'; import { URI } from 'vs/base/common/uri'; -import { BulkFileOperations } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview'; +import { BulkFileOperations } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview'; import { Range } from 'vs/editor/common/core/range'; +import { ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; suite('BulkEditPreview', function () { @@ -47,28 +47,25 @@ suite('BulkEditPreview', function () { test('one needsConfirmation unchecks all of file', async function () { - const edit: WorkspaceEdit = { - edits: [ - { newUri: URI.parse('some:///uri1'), metadata: { label: 'cat1', needsConfirmation: true } }, - { oldUri: URI.parse('some:///uri1'), newUri: URI.parse('some:///uri2'), metadata: { label: 'cat2', needsConfirmation: false } }, - ] - }; + const edits = [ + new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'cat1', needsConfirmation: true }), + new ResourceFileEdit(URI.parse('some:///uri1'), URI.parse('some:///uri2'), undefined, { label: 'cat2', needsConfirmation: false }), + ]; - const ops = await instaService.invokeFunction(BulkFileOperations.create, edit); + const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); assert.equal(ops.fileOperations.length, 1); - assert.equal(ops.checked.isChecked(edit.edits[0]), false); + assert.equal(ops.checked.isChecked(edits[0]), false); }); test('has categories', async function () { - const edit: WorkspaceEdit = { - edits: [ - { newUri: URI.parse('some:///uri1'), metadata: { label: 'uri1', needsConfirmation: true } }, - { newUri: URI.parse('some:///uri2'), metadata: { label: 'uri2', needsConfirmation: false } } - ] - }; + const edits = [ + new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'uri1', needsConfirmation: true }), + new ResourceFileEdit(undefined, URI.parse('some:///uri2'), undefined, { label: 'uri2', needsConfirmation: false }), + ]; - const ops = await instaService.invokeFunction(BulkFileOperations.create, edit); + + const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); assert.equal(ops.categories.length, 2); assert.equal(ops.categories[0].metadata.label, 'uri1'); // unconfirmed! assert.equal(ops.categories[1].metadata.label, 'uri2'); @@ -76,14 +73,12 @@ suite('BulkEditPreview', function () { test('has not categories', async function () { - const edit: WorkspaceEdit = { - edits: [ - { newUri: URI.parse('some:///uri1'), metadata: { label: 'uri1', needsConfirmation: true } }, - { newUri: URI.parse('some:///uri2'), metadata: { label: 'uri1', needsConfirmation: false } } - ] - }; + const edits = [ + new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'uri1', needsConfirmation: true }), + new ResourceFileEdit(undefined, URI.parse('some:///uri2'), undefined, { label: 'uri1', needsConfirmation: false }), + ]; - const ops = await instaService.invokeFunction(BulkFileOperations.create, edit); + const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); assert.equal(ops.categories.length, 1); assert.equal(ops.categories[0].metadata.label, 'uri1'); // unconfirmed! assert.equal(ops.categories[0].metadata.label, 'uri1'); @@ -91,43 +86,41 @@ suite('BulkEditPreview', function () { test('category selection', async function () { - const edit: WorkspaceEdit = { - edits: [ - { newUri: URI.parse('some:///uri1'), metadata: { label: 'C1', needsConfirmation: false } }, - { resource: URI.parse('some:///uri2'), edit: { text: 'foo', range: new Range(1, 1, 1, 1) }, metadata: { label: 'C2', needsConfirmation: false } } - ] - }; + const edits = [ + new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'C1', needsConfirmation: false }), + new ResourceTextEdit(URI.parse('some:///uri2'), { text: 'foo', range: new Range(1, 1, 1, 1) }, undefined, { label: 'C2', needsConfirmation: false }), + ]; - const ops = await instaService.invokeFunction(BulkFileOperations.create, edit); - assert.equal(ops.checked.isChecked(edit.edits[0]), true); - assert.equal(ops.checked.isChecked(edit.edits[1]), true); + const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); - assert.ok(edit === ops.getWorkspaceEdit()); + assert.equal(ops.checked.isChecked(edits[0]), true); + assert.equal(ops.checked.isChecked(edits[1]), true); + + assert.ok(edits === ops.getWorkspaceEdit()); // NOT taking to create, but the invalid text edit will // go through - ops.checked.updateChecked(edit.edits[0], false); - const newEdit = ops.getWorkspaceEdit(); - assert.ok(edit !== newEdit); + ops.checked.updateChecked(edits[0], false); + const newEdits = ops.getWorkspaceEdit(); + assert.ok(edits !== newEdits); - assert.equal(edit.edits.length, 2); - assert.equal(newEdit.edits.length, 1); + assert.equal(edits.length, 2); + assert.equal(newEdits.length, 1); }); test('fix bad metadata', async function () { // bogous edit that wants creation to be confirmed, but not it's textedit-child... - const edit: WorkspaceEdit = { - edits: [ - { newUri: URI.parse('some:///uri1'), metadata: { label: 'C1', needsConfirmation: true } }, - { resource: URI.parse('some:///uri1'), edit: { text: 'foo', range: new Range(1, 1, 1, 1) }, metadata: { label: 'C2', needsConfirmation: false } } - ] - }; - const ops = await instaService.invokeFunction(BulkFileOperations.create, edit); + const edits = [ + new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'C1', needsConfirmation: true }), + new ResourceTextEdit(URI.parse('some:///uri1'), { text: 'foo', range: new Range(1, 1, 1, 1) }, undefined, { label: 'C2', needsConfirmation: false }) + ]; - assert.equal(ops.checked.isChecked(edit.edits[0]), false); - assert.equal(ops.checked.isChecked(edit.edits[1]), false); + const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); + + assert.equal(ops.checked.isChecked(edits[0]), false); + assert.equal(ops.checked.isChecked(edits[1]), false); }); }); diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts index c32da9ad715..32c4c2f8b74 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts @@ -178,7 +178,11 @@ registerAction2(class extends EditorAction2 { menu: { id: MenuId.EditorContextPeek, group: 'navigation', - order: 1000 + order: 1000, + when: ContextKeyExpr.and( + _ctxHasCallHierarchyProvider, + PeekContext.notInPeekEditor + ), }, keybinding: { when: EditorContextKeys.editorTextFocus, diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index 069b114a77a..16f632117f0 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -10,7 +10,6 @@ import { CharCode } from 'vs/base/common/charCode'; import { Color } from 'vs/base/common/color'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; -import { escape } from 'vs/base/common/strings'; import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { Position } from 'vs/editor/common/core/position'; @@ -31,6 +30,8 @@ import { SemanticTokenRule, TokenStyleData, TokenStyle } from 'vs/platform/theme import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { SEMANTIC_HIGHLIGHTING_SETTING_ID, IEditorSemanticHighlightingOptions } from 'vs/editor/common/services/modelServiceImpl'; +const $ = dom.$; + class InspectEditorTokensController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.inspectEditorTokens'; @@ -151,23 +152,11 @@ function renderTokenText(tokenText: string): string { let charCode = tokenText.charCodeAt(charIndex); switch (charCode) { case CharCode.Tab: - result += '→'; + result += '\u2192'; // → break; case CharCode.Space: - result += '·'; - break; - - case CharCode.LessThan: - result += '<'; - break; - - case CharCode.GreaterThan: - result += '>'; - break; - - case CharCode.Ampersand: - result += '&'; + result += '\u00B7'; // · break; default: @@ -246,8 +235,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { if (this._isDisposed) { return; } - let text = this._compute(grammar, semanticTokens, position); - this._domNode.innerHTML = text; + this._compute(grammar, semanticTokens, position); this._domNode.style.maxWidth = `${Math.max(this._editor.getLayoutInfo().width * 0.66, 500)}px`; this._editor.layoutContentWidget(this); }, (err) => { @@ -268,11 +256,12 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { return this._themeService.getColorTheme().semanticHighlighting; } - private _compute(grammar: IGrammar | null, semanticTokens: SemanticTokensResult | null, position: Position): string { + private _compute(grammar: IGrammar | null, semanticTokens: SemanticTokensResult | null, position: Position) { const textMateTokenInfo = grammar && this._getTokensAtPosition(grammar, position); const semanticTokenInfo = semanticTokens && this._getSemanticTokenAtPosition(semanticTokens, position); if (!textMateTokenInfo && !semanticTokenInfo) { - return 'No grammar or semantic tokens available.'; + dom.reset(this._domNode, 'No grammar or semantic tokens available.'); + return; } let tmMetadata = textMateTokenInfo?.metadata; @@ -283,91 +272,125 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { const tokenText = semTokenText || tmTokenText || ''; - let result = ''; - result += `

    ${tokenText}(${tokenText.length} ${tokenText.length === 1 ? 'char' : 'chars'})

    `; - result += ``; - - result += ``; - result += ``; - result += ``; - - result += this._formatMetadata(semMetadata, tmMetadata); - result += ``; + dom.reset(this._domNode, + $('h2.tiw-token', undefined, + tokenText, + $('span.tiw-token-length', undefined, `${tokenText.length} ${tokenText.length === 1 ? 'char' : 'chars'}`))); + dom.append(this._domNode, $('hr.tiw-metadata-separator', { 'style': 'clear:both' })); + dom.append(this._domNode, $('table.tiw-metadata-table', undefined, + $('tbody', undefined, + $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'language'), + $('td.tiw-metadata-value', undefined, tmMetadata?.languageIdentifier.language || '') + ), + $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'standard token type' as string), + $('td.tiw-metadata-value', undefined, this._tokenTypeToString(tmMetadata?.tokenType || StandardTokenType.Other)) + ), + ...this._formatMetadata(semMetadata, tmMetadata) + ) + )); if (semanticTokenInfo) { - result += ``; - result += ``; - result += ``; + dom.append(this._domNode, $('hr.tiw-metadata-separator')); + const table = dom.append(this._domNode, $('table.tiw-metadata-table', undefined)); + const tbody = dom.append(table, $('tbody', undefined, + $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'semantic token type' as string), + $('td.tiw-metadata-value', undefined, semanticTokenInfo.type) + ) + )); if (semanticTokenInfo.modifiers.length) { - result += ``; + dom.append(tbody, $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'modifiers'), + $('td.tiw-metadata-value', undefined, semanticTokenInfo.modifiers.join(' ')), + )); } if (semanticTokenInfo.metadata) { const properties: (keyof TokenStyleData)[] = ['foreground', 'bold', 'italic', 'underline']; const propertiesByDefValue: { [rule: string]: string[] } = {}; - const allDefValues = []; // remember the order + const allDefValues = new Array<[Array, string]>(); // remember the order // first collect to detect when the same rule is used for multiple properties for (let property of properties) { if (semanticTokenInfo.metadata[property] !== undefined) { const definition = semanticTokenInfo.definitions[property]; const defValue = this._renderTokenStyleDefinition(definition, property); - let properties = propertiesByDefValue[defValue]; + const defValueStr = defValue.map(el => el instanceof HTMLElement ? el.outerHTML : el).join(); + let properties = propertiesByDefValue[defValueStr]; if (!properties) { - propertiesByDefValue[defValue] = properties = []; - allDefValues.push(defValue); + propertiesByDefValue[defValueStr] = properties = []; + allDefValues.push([defValue, defValueStr]); } properties.push(property); } } - for (let defValue of allDefValues) { - result += ``; + for (const [defValue, defValueStr] of allDefValues) { + dom.append(tbody, $('tr', undefined, + $('td.tiw-metadata-key', undefined, propertiesByDefValue[defValueStr].join(', ')), + $('td.tiw-metadata-value', undefined, ...defValue) + )); } } - result += ``; } if (textMateTokenInfo) { let theme = this._themeService.getColorTheme(); - result += ``; - result += ``; + dom.append(this._domNode, $('hr.tiw-metadata-separator')); + const table = dom.append(this._domNode, $('table.tiw-metadata-table')); + const tbody = dom.append(table, $('tbody')); + if (tmTokenText && tmTokenText !== tokenText) { - result += ``; + dom.append(tbody, $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'textmate token' as string), + $('td.tiw-metadata-value', undefined, `${tmTokenText} (${tmTokenText.length})`) + )); } - let scopes = ''; + const scopes = new Array(); for (let i = textMateTokenInfo.token.scopes.length - 1; i >= 0; i--) { - scopes += escape(textMateTokenInfo.token.scopes[i]); + scopes.push(textMateTokenInfo.token.scopes[i]); if (i > 0) { - scopes += '
    '; + scopes.push($('br')); } } - result += `
    `; + dom.append(tbody, $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'textmate scopes' as string), + $('td.tiw-metadata-value.tiw-metadata-scopes', undefined, ...scopes), + )); let matchingRule = findMatchingThemeRule(theme, textMateTokenInfo.token.scopes, false); const semForeground = semanticTokenInfo?.metadata?.foreground; if (matchingRule) { - let defValue = `${matchingRule.rawSelector}\n${JSON.stringify(matchingRule.settings, null, '\t')}`; if (semForeground !== textMateTokenInfo.metadata.foreground) { + let defValue = $('code.tiw-theme-selector', undefined, + matchingRule.rawSelector, $('br'), JSON.stringify(matchingRule.settings, null, '\t')); if (semForeground) { - defValue = `${defValue}`; + defValue = $('s', undefined, defValue); } - result += ``; + dom.append(tbody, $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'foreground'), + $('td.tiw-metadata-value', undefined, defValue), + )); } } else if (!semForeground) { - result += ``; + dom.append(tbody, $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'foreground'), + $('td.tiw-metadata-value', undefined, 'No theme selector' as string), + )); } - result += ``; } - return result; } - private _formatMetadata(semantic?: IDecodedMetadata, tm?: IDecodedMetadata) { - let result = ''; + private _formatMetadata(semantic?: IDecodedMetadata, tm?: IDecodedMetadata): Array { + const elements = new Array(); function render(property: 'foreground' | 'background') { let value = semantic?.[property] || tm?.[property]; if (value !== undefined) { const semanticStyle = semantic?.[property] ? 'tiw-metadata-semantic' : ''; - result += `${property}${value}`; - + elements.push($('tr', undefined, + $('td.tiw-metadata-key', undefined, property), + $(`td.tiw-metadata-value.${semanticStyle}`, undefined, value) + )); } return value; } @@ -377,17 +400,23 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { if (foreground && background) { const backgroundColor = Color.fromHex(background), foregroundColor = Color.fromHex(foreground); if (backgroundColor.isOpaque()) { - result += `contrast ratio${backgroundColor.getContrastRatio(foregroundColor.makeOpaque(backgroundColor)).toFixed(2)}`; + elements.push($('tr', undefined, + $('td.tiw-metadata-key', undefined, 'contrast ratio' as string), + $('td.tiw-metadata-value', undefined, backgroundColor.getContrastRatio(foregroundColor.makeOpaque(backgroundColor)).toFixed(2)) + )); } else { - result += 'Contrast ratio cannot be precise for background colors that use transparency'; + elements.push($('tr', undefined, + $('td.tiw-metadata-key', undefined, 'Contrast ratio cannot be precise for background colors that use transparency' as string), + $('td.tiw-metadata-value') + )); } } - let fontStyleLabels: string[] = []; + const fontStyleLabels = new Array(); function addStyle(key: 'bold' | 'italic' | 'underline') { if (semantic && semantic[key]) { - fontStyleLabels.push(``); + fontStyleLabels.push($('span.tiw-metadata-semantic', undefined, key)); } else if (tm && tm[key]) { fontStyleLabels.push(key); } @@ -396,9 +425,12 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { addStyle('italic'); addStyle('underline'); if (fontStyleLabels.length) { - result += `font style${fontStyleLabels.join(' ')}`; + elements.push($('tr', undefined, + $('td.tiw-metadata-key', undefined, 'font style' as string), + $('td.tiw-metadata-value', undefined, fontStyleLabels.join(' ')) + )); } - return result; + return elements; } private _decodeMetadata(metadata: number): IDecodedMetadata { @@ -425,8 +457,8 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { case StandardTokenType.Comment: return 'Comment'; case StandardTokenType.String: return 'String'; case StandardTokenType.RegEx: return 'RegEx'; + default: return '??'; } - return '??'; } private _getTokensAtPosition(grammar: IGrammar, position: Position): ITextMateTokenInfo { @@ -549,9 +581,10 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { return null; } - private _renderTokenStyleDefinition(definition: TokenStyleDefinition | undefined, property: keyof TokenStyleData): string { + private _renderTokenStyleDefinition(definition: TokenStyleDefinition | undefined, property: keyof TokenStyleData): Array { + const elements = new Array(); if (definition === undefined) { - return ''; + return elements; } const theme = this._themeService.getColorTheme() as ColorThemeData; @@ -561,20 +594,27 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { const matchingRule = scopesDefinition[property]; if (matchingRule && scopesDefinition.scope) { const strScopes = Array.isArray(matchingRule.scope) ? matchingRule.scope.join(', ') : String(matchingRule.scope); - return `${escape(scopesDefinition.scope.join(' '))}
    ${strScopes}\n${JSON.stringify(matchingRule.settings, null, '\t')}`; + elements.push( + scopesDefinition.scope.join(' '), + $('br'), + $('code.tiw-theme-selector', undefined, strScopes, $('br'), JSON.stringify(matchingRule.settings, null, '\t'))); + return elements; } - return ''; + return elements; } else if (SemanticTokenRule.is(definition)) { const scope = theme.getTokenStylingRuleScope(definition); if (scope === 'setting') { - return `User settings: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`; + elements.push(`User settings: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`); + return elements; } else if (scope === 'theme') { - return `Color theme: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`; + elements.push(`Color theme: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`); + return elements; } - return ''; + return elements; } else { const style = theme.resolveTokenStyleValue(definition); - return `Default: ${style ? this._renderStyleProperty(style, property) : ''}`; + elements.push(`Default: ${style ? this._renderStyleProperty(style, property) : ''}`); + return elements; } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts index 473166ff599..83a504c0135 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts @@ -5,7 +5,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import * as strings from 'vs/base/common/strings'; -import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IActiveCodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; import { EditOperation } from 'vs/editor/common/core/editOperation'; @@ -13,11 +13,11 @@ 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 { ITextModel } from 'vs/editor/common/model'; -import { CodeActionTriggerType, DocumentFormattingEditProvider, CodeActionProvider } from 'vs/editor/common/modes'; +import { CodeActionTriggerType, CodeActionProvider } from 'vs/editor/common/modes'; import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction'; import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; import { CodeActionKind } from 'vs/editor/contrib/codeAction/types'; -import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; +import { formatDocumentRangesWithSelectedProvider, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -29,6 +29,8 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution, Extensions as WorkbenchContributionsExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { getModifiedRanges } from 'vs/workbench/contrib/format/browser/formatModified'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export class TrimWhitespaceParticipant implements ITextFileSaveParticipant { @@ -221,15 +223,14 @@ class FormatOnSaveParticipant implements ITextFileSaveParticipant { if (!model.textEditorModel) { return; } + if (env.reason === SaveReason.AUTO) { + return undefined; + } const textEditorModel = model.textEditorModel; const overrides = { overrideIdentifier: textEditorModel.getLanguageIdentifier().language, resource: textEditorModel.uri }; - if (env.reason === SaveReason.AUTO || !this.configurationService.getValue('editor.formatOnSave', overrides)) { - return undefined; - } - - const nestedProgress = new Progress(provider => { + const nestedProgress = new Progress<{ displayName?: string, extensionId?: ExtensionIdentifier }>(provider => { progress.report({ message: localize( 'formatting', @@ -238,8 +239,24 @@ class FormatOnSaveParticipant implements ITextFileSaveParticipant { ) }); }); + + const enabled = this.configurationService.getValue('editor.formatOnSave', overrides); + if (!enabled) { + return undefined; + } + const editorOrModel = findEditor(textEditorModel, this.codeEditorService) || textEditorModel; - await this.instantiationService.invokeFunction(formatDocumentWithSelectedProvider, editorOrModel, FormattingMode.Silent, nestedProgress, token); + const mode = this.configurationService.getValue<'file' | 'modifications'>('editor.formatOnSaveMode', overrides); + if (mode === 'modifications') { + // format modifications + const ranges = await this.instantiationService.invokeFunction(getModifiedRanges, isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel); + if (ranges) { + await this.instantiationService.invokeFunction(formatDocumentRangesWithSelectedProvider, editorOrModel, ranges, FormattingMode.Silent, nestedProgress, token); + } + } else { + // format the whole file + await this.instantiationService.invokeFunction(formatDocumentWithSelectedProvider, editorOrModel, FormattingMode.Silent, nestedProgress, token); + } } } diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index ec11e213ac4..490a1a53d87 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -535,11 +535,11 @@ function fillInActions(groups: [string, Array(target) ? target : target.primary; + const to = Array.isArray(target) ? target : target.primary; to.unshift(...actions); } else { - const to = Array.isArray(target) ? target : target.secondary; + const to = Array.isArray(target) ? target : target.secondary; if (to.length > 0) { to.push(new Separator()); diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 0abbf8b7626..8a2f4de0c2b 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -120,7 +120,7 @@ export class CommentNodeRenderer implements IListRenderer renderElement(node: ITreeNode, index: number, templateData: ICommentThreadTemplateData, height: number | undefined): void { templateData.userName.textContent = node.element.comment.userName; - templateData.commentText.innerHTML = ''; + templateData.commentText.innerText = ''; const disposables = new DisposableStore(); templateData.disposables.push(disposables); const renderedComment = renderMarkdown(node.element.comment.body, { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 8a98767b7f5..71aecb597c2 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -32,7 +32,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { isSafari } from 'vs/base/browser/browser'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { registerThemingParticipant, themeColorFromId } from 'vs/platform/theme/common/themeService'; import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -87,7 +87,7 @@ function getBreakpointDecorationOptions(model: ITextModel, breakpoint: IBreakpoi let overviewRulerDecoration: IModelDecorationOverviewRulerOptions | null = null; if (showBreakpointsInOverviewRuler) { overviewRulerDecoration = { - color: 'rgb(124, 40, 49)', + color: themeColorFromId(debugIconBreakpointForeground), position: OverviewRulerLane.Left }; } @@ -167,12 +167,17 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi @ILabelService private readonly labelService: ILabelService ) { this.breakpointWidgetVisible = CONTEXT_BREAKPOINT_WIDGET_VISIBLE.bindTo(contextKeyService); - this.registerListeners(); this.setDecorationsScheduler = new RunOnceScheduler(() => this.setDecorations(), 30); + this.registerListeners(); + this.setDecorationsScheduler.schedule(); } private registerListeners(): void { this.toDispose.push(this.editor.onMouseDown(async (e: IEditorMouseEvent) => { + if (!this.debugService.getConfigurationManager().hasDebuggers()) { + return; + } + const data = e.target.detail as IMarginData; const model = this.editor.getModel(); if (!e.target.position || !model || e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN || data.isAfterLines || !this.marginFreeFromNonDebugDecorations(e.target.position.lineNumber)) { @@ -247,6 +252,10 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi * 2. When users click on line numbers, the breakpoint hint displays immediately, however it doesn't create the breakpoint unless users click on the left gutter. On a touch screen, it's hard to click on that small area. */ this.toDispose.push(this.editor.onMouseMove((e: IEditorMouseEvent) => { + if (!this.debugService.getConfigurationManager().hasDebuggers()) { + return; + } + let showBreakpointHintAtLineNumber = -1; const model = this.editor.getModel(); if (model && e.target.position && (e.target.type === MouseTargetType.GUTTER_GLYPH_MARGIN || e.target.type === MouseTargetType.GUTTER_LINE_NUMBERS) && this.debugService.getConfigurationManager().canSetBreakpointsIn(model) && diff --git a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts index bc1cac85a06..b0c25ff46dd 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts @@ -40,7 +40,7 @@ const FOCUSED_STACK_FRAME_DECORATION: IModelDecorationOptions = { stickiness }; -export function createDecorationsForStackFrame(stackFrame: IStackFrame, topStackFrameRange: IRange | undefined): IModelDeltaDecoration[] { +export function createDecorationsForStackFrame(stackFrame: IStackFrame, topStackFrameRange: IRange | undefined, isFocusedSession: boolean): IModelDeltaDecoration[] { // only show decorations for the currently focused thread. const result: IModelDeltaDecoration[] = []; const columnUntilEOLRange = new Range(stackFrame.range.startLineNumber, stackFrame.range.startColumn, stackFrame.range.startLineNumber, Constants.MAX_SAFE_SMALL_INTEGER); @@ -48,12 +48,14 @@ export function createDecorationsForStackFrame(stackFrame: IStackFrame, topStack // compute how to decorate the editor. Different decorations are used if this is a top stack frame, focused stack frame, // an exception or a stack frame that did not change the line number (we only decorate the columns, not the whole line). - const callStack = stackFrame.thread.getCallStack(); - if (callStack && callStack.length && stackFrame === callStack[0]) { - result.push({ - options: TOP_STACK_FRAME_MARGIN, - range - }); + const topStackFrame = stackFrame.thread.getTopStackFrame(); + if (stackFrame.getId() === topStackFrame?.getId()) { + if (isFocusedSession) { + result.push({ + options: TOP_STACK_FRAME_MARGIN, + range + }); + } result.push({ options: TOP_STACK_FRAME_DECORATION, @@ -68,10 +70,12 @@ export function createDecorationsForStackFrame(stackFrame: IStackFrame, topStack } topStackFrameRange = columnUntilEOLRange; } else { - result.push({ - options: FOCUSED_STACK_FRAME_MARGIN, - range - }); + if (isFocusedSession) { + result.push({ + options: FOCUSED_STACK_FRAME_MARGIN, + range + }); + } result.push({ options: FOCUSED_STACK_FRAME_DECORATION, @@ -106,6 +110,7 @@ export class CallStackEditorContribution implements IEditorContribution { const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; const decorations: IModelDeltaDecoration[] = []; this.debugService.getModel().getSessions().forEach(s => { + const isSessionFocused = s === focusedStackFrame?.thread.session; s.getAllThreads().forEach(t => { if (t.stopped) { let candidateStackFrame = t === focusedStackFrame?.thread ? focusedStackFrame : undefined; @@ -117,7 +122,7 @@ export class CallStackEditorContribution implements IEditorContribution { } if (candidateStackFrame && candidateStackFrame.source.uri.toString() === this.editor.getModel()?.uri.toString()) { - decorations.push(...createDecorationsForStackFrame(candidateStackFrame, this.topStackFrameRange)); + decorations.push(...createDecorationsForStackFrame(candidateStackFrame, this.topStackFrameRange, isSessionFocused)); } } }); diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 96b2a9827d9..23009759915 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -161,7 +161,6 @@ export class CallStackView extends ViewPane { dom.toggleClass(this.pauseMessageLabel, 'exception', thread.stoppedDetails.reason === 'exception'); this.pauseMessage.hidden = false; this.updateActions(); - } else { this.pauseMessage.hidden = true; this.updateActions(); @@ -222,7 +221,7 @@ export class CallStackView extends ViewPane { new ThreadsRenderer(this.instantiationService), this.instantiationService.createInstance(StackFramesRenderer), new ErrorsRenderer(), - new LoadMoreRenderer(this.themeService), + new LoadAllRenderer(this.themeService), new ShowMoreRenderer(this.themeService) ], this.dataSource, { accessibilityProvider: new CallStackAccessibilityProvider(), @@ -252,7 +251,7 @@ export class CallStackView extends ViewPane { return e; } if (e instanceof ThreadAndSessionIds) { - return LoadMoreRenderer.LABEL; + return LoadAllRenderer.LABEL; } return nls.localize('showMoreStackFrames2', "Show More Stack Frames"); @@ -273,7 +272,7 @@ export class CallStackView extends ViewPane { this.tree.setInput(this.debugService.getModel()); - this._register(this.tree.onDidOpen(e => { + this._register(this.tree.onDidOpen(async e => { if (this.ignoreSelectionChangedEvent) { return; } @@ -302,8 +301,11 @@ export class CallStackView extends ViewPane { const session = this.debugService.getModel().getSession(element.sessionId); const thread = session && session.getThread(element.threadId); if (thread) { - (thread).fetchCallStack() - .then(() => this.tree.updateChildren()); + const totalFrames = thread.stoppedDetails?.totalFrames; + const remainingFramesCount = typeof totalFrames === 'number' ? (totalFrames - thread.getCallStack().length) : undefined; + // Get all the remaining frames + await (thread).fetchCallStack(remainingFramesCount); + await this.tree.updateChildren(); } } if (element instanceof Array) { @@ -486,8 +488,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer setActionBar())); - data.stateLabel.hidden = false; + data.stateLabel.style.display = ''; if (thread && thread.stoppedDetails) { data.stateLabel.textContent = thread.stoppedDetails.description || nls.localize('debugStopped', "Paused on {0}", thread.stoppedDetails.reason || ''); - } else { - const hasChildSessions = this.debugService.getModel().getSessions().find(s => s.parentSession === session); - if (!hasChildSessions) { - data.stateLabel.textContent = nls.localize({ key: 'running', comment: ['indicates state'] }, "Running"); - } else { - data.stateLabel.hidden = true; + if (thread.stoppedDetails.text) { + data.session.title = thread.stoppedDetails.text; } + } else { + data.stateLabel.textContent = nls.localize({ key: 'running', comment: ['indicates state'] }, "Running"); } } @@ -704,18 +703,18 @@ class ErrorsRenderer implements ICompressibleTreeRenderer { - static readonly ID = 'loadMore'; - static readonly LABEL = nls.localize('loadMoreStackFrames', "Load More Stack Frames"); +class LoadAllRenderer implements ICompressibleTreeRenderer { + static readonly ID = 'loadAll'; + static readonly LABEL = nls.localize('loadAllStackFrames', "Load All Stack Frames"); constructor(private readonly themeService: IThemeService) { } get templateId(): string { - return LoadMoreRenderer.ID; + return LoadAllRenderer.ID; } renderTemplate(container: HTMLElement): ILabelTemplateData { - const label = dom.append(container, $('.load-more')); + const label = dom.append(container, $('.load-all')); const toDispose = attachStylerCallback(this.themeService, { textLinkForeground }, colors => { if (colors.textLinkForeground) { label.style.color = colors.textLinkForeground.toString(); @@ -726,7 +725,7 @@ class LoadMoreRenderer implements ICompressibleTreeRenderer, index: number, data: ILabelTemplateData): void { - data.label.textContent = LoadMoreRenderer.LABEL; + data.label.textContent = LoadAllRenderer.LABEL; } renderCompressedElements(node: ITreeNode, FuzzyScore>, index: number, templateData: ILabelTemplateData, height: number | undefined): void { @@ -804,7 +803,7 @@ class CallStackDelegate implements IListVirtualDelegate { return ErrorsRenderer.ID; } if (element instanceof ThreadAndSessionIds) { - return LoadMoreRenderer.ID; + return LoadAllRenderer.ID; } // element instanceof Array @@ -899,29 +898,27 @@ class CallStackDataSource implements IAsyncDataSource> { + private async getThreadCallstack(thread: Thread): Promise> { let callStack: any[] = thread.getCallStack(); - let callStackPromise: Promise = Promise.resolve(null); if (!callStack || !callStack.length) { - callStackPromise = thread.fetchCallStack().then(() => callStack = thread.getCallStack()); + await thread.fetchCallStack(); + callStack = thread.getCallStack(); } - return callStackPromise.then(() => { - if (callStack.length === 1 && thread.session.capabilities.supportsDelayedStackTraceLoading && thread.stoppedDetails && thread.stoppedDetails.totalFrames && thread.stoppedDetails.totalFrames > 1) { - // To reduce flashing of the call stack view simply append the stale call stack - // once we have the correct data the tree will refresh and we will no longer display it. - callStack = callStack.concat(thread.getStaleCallStack().slice(1)); - } + if (callStack.length === 1 && thread.session.capabilities.supportsDelayedStackTraceLoading && thread.stoppedDetails && thread.stoppedDetails.totalFrames && thread.stoppedDetails.totalFrames > 1) { + // To reduce flashing of the call stack view simply append the stale call stack + // once we have the correct data the tree will refresh and we will no longer display it. + callStack = callStack.concat(thread.getStaleCallStack().slice(1)); + } - if (thread.stoppedDetails && thread.stoppedDetails.framesErrorMessage) { - callStack = callStack.concat([thread.stoppedDetails.framesErrorMessage]); - } - if (thread.stoppedDetails && thread.stoppedDetails.totalFrames && thread.stoppedDetails.totalFrames > callStack.length && callStack.length > 1) { - callStack = callStack.concat([new ThreadAndSessionIds(thread.session.getId(), thread.threadId)]); - } + if (thread.stoppedDetails && thread.stoppedDetails.framesErrorMessage) { + callStack = callStack.concat([thread.stoppedDetails.framesErrorMessage]); + } + if (thread.stoppedDetails && thread.stoppedDetails.totalFrames && thread.stoppedDetails.totalFrames > callStack.length && callStack.length > 1) { + callStack = callStack.concat([new ThreadAndSessionIds(thread.session.getId(), thread.threadId)]); + } - return callStack; - }); + return callStack; } } @@ -949,7 +946,7 @@ class CallStackAccessibilityProvider implements IListAccessibilityProvider(ViewExtensions.ViewContainersRegistry).registerViewContainer({ - id: VIEWLET_ID, - name: nls.localize('run', "Run"), - ctorDescriptor: new SyncDescriptor(DebugViewPaneContainer), - icon: Codicon.debugAlt.classNames, - alwaysUseContainerInfo: true, - order: 2 -}, ViewContainerLocation.Sidebar); - -const openViewletKb: IKeybindings = { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_D -}; -const openPanelKb: IKeybindings = { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Y -}; - -// register repl panel - -const VIEW_CONTAINER: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ - id: DEBUG_PANEL_ID, - name: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugPanel' }, 'Debug Console'), - icon: Codicon.debugConsole.classNames, - ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [DEBUG_PANEL_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), - storageId: DEBUG_PANEL_ID, - focusCommand: { - id: OpenDebugConsoleAction.ID, - keybindings: openPanelKb - }, - order: 2, - hideIfEmpty: true -}, ViewContainerLocation.Panel); - -Registry.as(ViewExtensions.ViewsRegistry).registerViews([{ - id: REPL_VIEW_ID, - name: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugPanel' }, 'Debug Console'), - containerIcon: Codicon.debugConsole.classNames, - canToggleVisibility: false, - canMoveView: true, - ctorDescriptor: new SyncDescriptor(Repl), -}], VIEW_CONTAINER); - -// Register default debug views -const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); -viewsRegistry.registerViews([{ id: VARIABLES_VIEW_ID, name: nls.localize('variables', "Variables"), containerIcon: Codicon.debugAlt.classNames, ctorDescriptor: new SyncDescriptor(VariablesView), order: 10, weight: 40, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusVariablesView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); -viewsRegistry.registerViews([{ id: WATCH_VIEW_ID, name: nls.localize('watch', "Watch"), containerIcon: Codicon.debugAlt.classNames, ctorDescriptor: new SyncDescriptor(WatchExpressionsView), order: 20, weight: 10, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusWatchView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); -viewsRegistry.registerViews([{ id: CALLSTACK_VIEW_ID, name: nls.localize('callStack', "Call Stack"), containerIcon: Codicon.debugAlt.classNames, ctorDescriptor: new SyncDescriptor(CallStackView), order: 30, weight: 30, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusCallStackView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); -viewsRegistry.registerViews([{ id: BREAKPOINTS_VIEW_ID, name: nls.localize('breakpoints', "Breakpoints"), containerIcon: Codicon.debugAlt.classNames, ctorDescriptor: new SyncDescriptor(BreakpointsView), order: 40, weight: 20, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusBreakpointsView' }, when: ContextKeyExpr.or(CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_UX.isEqualTo('default')) }], viewContainer); -viewsRegistry.registerViews([{ id: WelcomeView.ID, name: WelcomeView.LABEL, containerIcon: Codicon.debugAlt.classNames, ctorDescriptor: new SyncDescriptor(WelcomeView), order: 1, weight: 40, canToggleVisibility: true, when: CONTEXT_DEBUG_UX.isEqualTo('simple') }], viewContainer); -viewsRegistry.registerViews([{ id: LOADED_SCRIPTS_VIEW_ID, name: nls.localize('loadedScripts', "Loaded Scripts"), containerIcon: Codicon.debugAlt.classNames, ctorDescriptor: new SyncDescriptor(LoadedScriptsView), order: 35, weight: 5, canToggleVisibility: true, canMoveView: true, collapsed: true, when: ContextKeyExpr.and(CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_DEBUG_UX.isEqualTo('default')) }], viewContainer); - -registerCommands(); - -// register action to open viewlet const registry = Registry.as(WorkbenchActionRegistryExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenDebugConsoleAction, openPanelKb), 'View: Debug Console', nls.localize('view', "View")); -registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenDebugViewletAction, openViewletKb), 'View: Show Run and Debug', nls.localize('view', "View")); - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugToolBar, LifecyclePhase.Restored); -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugContentProvider, LifecyclePhase.Eventually); -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(StatusBarColorProvider, LifecyclePhase.Eventually); - const debugCategory = nls.localize('debugCategory', "Debug"); const runCategroy = nls.localize('runCategory', "Run"); +registerWorkbenchContributions(); +registerColors(); +registerCommandsAndActions(); +registerDebugMenu(); +registerEditorActions(); +registerCommands(); +registerDebugPanel(); +registry.registerWorkbenchAction(SyncActionDescriptor.from(StartAction, { primary: KeyCode.F5 }, CONTEXT_IN_DEBUG_MODE.toNegated()), 'Debug: Start Debugging', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); +registry.registerWorkbenchAction(SyncActionDescriptor.from(RunAction, { primary: KeyMod.CtrlCmd | KeyCode.F5, mac: { primary: KeyMod.WinCtrl | KeyCode.F5 } }), 'Run: Start Without Debugging', runCategroy, CONTEXT_DEBUGGERS_AVAILABLE); -registry.registerWorkbenchAction(SyncActionDescriptor.from(StartAction, { primary: KeyCode.F5 }, CONTEXT_IN_DEBUG_MODE.toNegated()), 'Debug: Start Debugging', debugCategory); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ConfigureAction), 'Debug: Open launch.json', debugCategory); -registry.registerWorkbenchAction(SyncActionDescriptor.from(AddFunctionBreakpointAction), 'Debug: Add Function Breakpoint', debugCategory); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ReapplyBreakpointsAction), 'Debug: Reapply All Breakpoints', debugCategory); -registry.registerWorkbenchAction(SyncActionDescriptor.from(RunAction, { primary: KeyMod.CtrlCmd | KeyCode.F5, mac: { primary: KeyMod.WinCtrl | KeyCode.F5 } }), 'Run: Start Without Debugging', runCategroy); -registry.registerWorkbenchAction(SyncActionDescriptor.from(RemoveAllBreakpointsAction), 'Debug: Remove All Breakpoints', debugCategory); -registry.registerWorkbenchAction(SyncActionDescriptor.from(EnableAllBreakpointsAction), 'Debug: Enable All Breakpoints', debugCategory); -registry.registerWorkbenchAction(SyncActionDescriptor.from(DisableAllBreakpointsAction), 'Debug: Disable All Breakpoints', debugCategory); -registry.registerWorkbenchAction(SyncActionDescriptor.from(SelectAndStartAction), 'Debug: Select and Start Debugging', debugCategory); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ClearReplAction), 'Debug: Clear Console', debugCategory); +registerSingleton(IDebugService, DebugService, true); +registerDebugView(); +registerConfiguration(); +regsiterEditorContributions(); -const registerDebugCommandPaletteItem = (id: string, title: string, when?: ContextKeyExpression, precondition?: ContextKeyExpression) => { - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - when, - command: { - id, - title: `Debug: ${title}`, - precondition - } - }); -}; - -registerDebugCommandPaletteItem(RESTART_SESSION_ID, RESTART_LABEL); -registerDebugCommandPaletteItem(TERMINATE_THREAD_ID, nls.localize('terminateThread', "Terminate Thread"), CONTEXT_IN_DEBUG_MODE); -registerDebugCommandPaletteItem(STEP_OVER_ID, STEP_OVER_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugCommandPaletteItem(STEP_INTO_ID, STEP_INTO_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugCommandPaletteItem(STEP_OUT_ID, STEP_OUT_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugCommandPaletteItem(PAUSE_ID, PAUSE_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('running')); -registerDebugCommandPaletteItem(DISCONNECT_ID, DISCONNECT_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_FOCUSED_SESSION_IS_ATTACH); -registerDebugCommandPaletteItem(STOP_ID, STOP_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated()); -registerDebugCommandPaletteItem(CONTINUE_ID, CONTINUE_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugCommandPaletteItem(FOCUS_REPL_ID, nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugFocusConsole' }, 'Focus on Debug Console View')); -registerDebugCommandPaletteItem(JUMP_TO_CURSOR_ID, nls.localize('jumpToCursor', "Jump to Cursor"), CONTEXT_JUMP_TO_CURSOR_SUPPORTED); -registerDebugCommandPaletteItem(JUMP_TO_CURSOR_ID, nls.localize('SetNextStatement', "Set Next Statement"), CONTEXT_JUMP_TO_CURSOR_SUPPORTED); -registerDebugCommandPaletteItem(RunToCursorAction.ID, RunToCursorAction.LABEL, ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped'))); -registerDebugCommandPaletteItem(TOGGLE_INLINE_BREAKPOINT_ID, nls.localize('inlineBreakpoint', "Inline Breakpoint")); - - -// Register Quick Access -Registry.as(QuickAccessExtensions.Quickaccess).registerQuickAccessProvider({ - ctor: StartDebugQuickAccessProvider, - prefix: StartDebugQuickAccessProvider.PREFIX, - contextKey: 'inLaunchConfigurationsPicker', - placeholder: nls.localize('startDebugPlaceholder', "Type the name of a launch configuration to run."), - helpEntries: [{ description: nls.localize('startDebuggingHelp', "Start Debugging"), needsEditor: false }] -}); - -// register service -registerSingleton(IDebugService, service.DebugService, true); - -// Register configuration -const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); -configurationRegistry.registerConfiguration({ - id: 'debug', - order: 20, - title: nls.localize('debugConfigurationTitle', "Debug"), - type: 'object', - properties: { - 'debug.allowBreakpointsEverywhere': { - type: 'boolean', - description: nls.localize({ comment: ['This is the description for a setting'], key: 'allowBreakpointsEverywhere' }, "Allow setting breakpoints in any file."), - default: false - }, - 'debug.openExplorerOnEnd': { - type: 'boolean', - description: nls.localize({ comment: ['This is the description for a setting'], key: 'openExplorerOnEnd' }, "Automatically open the explorer view at the end of a debug session."), - default: false - }, - 'debug.inlineValues': { - type: 'boolean', - description: nls.localize({ comment: ['This is the description for a setting'], key: 'inlineValues' }, "Show variable values inline in editor while debugging."), - default: false - }, - 'debug.toolBarLocation': { - enum: ['floating', 'docked', 'hidden'], - markdownDescription: nls.localize({ comment: ['This is the description for a setting'], key: 'toolBarLocation' }, "Controls the location of the debug toolbar. Either `floating` in all views, `docked` in the debug view, or `hidden`."), - default: 'floating' - }, - 'debug.showInStatusBar': { - enum: ['never', 'always', 'onFirstSessionStart'], - enumDescriptions: [nls.localize('never', "Never show debug in status bar"), nls.localize('always', "Always show debug in status bar"), nls.localize('onFirstSessionStart', "Show debug in status bar only after debug was started for the first time")], - description: nls.localize({ comment: ['This is the description for a setting'], key: 'showInStatusBar' }, "Controls when the debug status bar should be visible."), - default: 'onFirstSessionStart' - }, - 'debug.internalConsoleOptions': INTERNAL_CONSOLE_OPTIONS_SCHEMA, - 'debug.console.closeOnEnd': { - type: 'boolean', - description: nls.localize('debug.console.closeOnEnd', "Controls if the debug console should be automatically closed when the debug session ends."), - default: false - }, - 'debug.openDebug': { - enum: ['neverOpen', 'openOnSessionStart', 'openOnFirstSessionStart', 'openOnDebugBreak'], - default: 'openOnFirstSessionStart', - description: nls.localize('openDebug', "Controls when the debug view should open.") - }, - 'debug.showSubSessionsInToolBar': { - type: 'boolean', - description: nls.localize({ comment: ['This is the description for a setting'], key: 'showSubSessionsInToolBar' }, "Controls whether the debug sub-sessions are shown in the debug tool bar. When this setting is false the stop command on a sub-session will also stop the parent session."), - default: false - }, - 'debug.console.fontSize': { - type: 'number', - description: nls.localize('debug.console.fontSize', "Controls the font size in pixels in the debug console."), - default: isMacintosh ? 12 : 14, - }, - 'debug.console.fontFamily': { - type: 'string', - description: nls.localize('debug.console.fontFamily', "Controls the font family in the debug console."), - default: 'default' - }, - 'debug.console.lineHeight': { - type: 'number', - description: nls.localize('debug.console.lineHeight', "Controls the line height in pixels in the debug console. Use 0 to compute the line height from the font size."), - default: 0 - }, - 'debug.console.wordWrap': { - type: 'boolean', - description: nls.localize('debug.console.wordWrap', "Controls if the lines should wrap in the debug console."), - default: true - }, - 'debug.console.historySuggestions': { - type: 'boolean', - description: nls.localize('debug.console.historySuggestions', "Controls if the debug console should suggest previously typed input."), - default: true - }, - 'launch': { - type: 'object', - description: nls.localize({ comment: ['This is the description for a setting'], key: 'launch' }, "Global debug launch configuration. Should be used as an alternative to 'launch.json' that is shared across workspaces."), - default: { configurations: [], compounds: [] }, - $ref: launchSchemaId - }, - 'debug.focusWindowOnBreak': { - type: 'boolean', - description: nls.localize('debug.focusWindowOnBreak', "Controls whether the workbench window should be focused when the debugger breaks."), - default: true - }, - 'debug.onTaskErrors': { - enum: ['debugAnyway', 'showErrors', 'prompt', 'abort'], - enumDescriptions: [nls.localize('debugAnyway', "Ignore task errors and start debugging."), nls.localize('showErrors', "Show the Problems view and do not start debugging."), nls.localize('prompt', "Prompt user."), nls.localize('cancel', "Cancel debugging.")], - description: nls.localize('debug.onTaskErrors', "Controls what to do when errors are encountered after running a preLaunchTask."), - default: 'prompt' - }, - 'debug.showBreakpointsInOverviewRuler': { - type: 'boolean', - description: nls.localize({ comment: ['This is the description for a setting'], key: 'showBreakpointsInOverviewRuler' }, "Controls whether breakpoints should be shown in the overview ruler."), - default: false - }, - 'debug.showInlineBreakpointCandidates': { - type: 'boolean', - description: nls.localize({ comment: ['This is the description for a setting'], key: 'showInlineBreakpointCandidates' }, "Controls whether inline breakpoints candidate decorations should be shown in the editor while debugging."), - default: true - } +function registerWorkbenchContributions(): void { + // Register Debug Workbench Contributions + Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugStatusContribution, LifecyclePhase.Eventually); + Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugProgressContribution, LifecyclePhase.Eventually); + if (isWeb) { + Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugTitleContribution, LifecyclePhase.Eventually); } -}); + Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugToolBar, LifecyclePhase.Restored); + Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugContentProvider, LifecyclePhase.Eventually); + Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(StatusBarColorProvider, LifecyclePhase.Eventually); + + // Register Quick Access + Registry.as(QuickAccessExtensions.Quickaccess).registerQuickAccessProvider({ + ctor: StartDebugQuickAccessProvider, + prefix: StartDebugQuickAccessProvider.PREFIX, + contextKey: 'inLaunchConfigurationsPicker', + placeholder: nls.localize('startDebugPlaceholder', "Type the name of a launch configuration to run."), + helpEntries: [{ description: nls.localize('startDebuggingHelp', "Start Debugging"), needsEditor: false }] + }); -// Register Debug Workbench Contributions -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugStatusContribution, LifecyclePhase.Eventually); -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugProgressContribution, LifecyclePhase.Eventually); -if (isWeb) { - Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugTitleContribution, LifecyclePhase.Eventually); } -// Debug toolbar +function regsiterEditorContributions(): void { + registerEditorContribution('editor.contrib.callStack', CallStackEditorContribution); + registerEditorContribution(BREAKPOINT_EDITOR_CONTRIBUTION_ID, BreakpointEditorContribution); + registerEditorContribution(EDITOR_CONTRIBUTION_ID, DebugEditorContribution); +} -const registerDebugToolBarItem = (id: string, title: string, order: number, icon: { light?: URI, dark?: URI } | ThemeIcon, when?: ContextKeyExpression, precondition?: ContextKeyExpression) => { - MenuRegistry.appendMenuItem(MenuId.DebugToolBar, { - group: 'navigation', - when, - order, - command: { - id, - title, - icon, - precondition - } - }); -}; +function registerCommandsAndActions(): void { -registerDebugToolBarItem(CONTINUE_ID, CONTINUE_LABEL, 10, { id: 'codicon/debug-continue' }, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugToolBarItem(PAUSE_ID, PAUSE_LABEL, 10, { id: 'codicon/debug-pause' }, CONTEXT_DEBUG_STATE.notEqualsTo('stopped')); -registerDebugToolBarItem(STOP_ID, STOP_LABEL, 70, { id: 'codicon/debug-stop' }, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated()); -registerDebugToolBarItem(DISCONNECT_ID, DISCONNECT_LABEL, 70, { id: 'codicon/debug-disconnect' }, CONTEXT_FOCUSED_SESSION_IS_ATTACH); -registerDebugToolBarItem(STEP_OVER_ID, STEP_OVER_LABEL, 20, { id: 'codicon/debug-step-over' }, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugToolBarItem(STEP_INTO_ID, STEP_INTO_LABEL, 30, { id: 'codicon/debug-step-into' }, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugToolBarItem(STEP_OUT_ID, STEP_OUT_LABEL, 40, { id: 'codicon/debug-step-out' }, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugToolBarItem(RESTART_SESSION_ID, RESTART_LABEL, 60, { id: 'codicon/debug-restart' }); -registerDebugToolBarItem(STEP_BACK_ID, nls.localize('stepBackDebug', "Step Back"), 50, { id: 'codicon/debug-step-back' }, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugToolBarItem(REVERSE_CONTINUE_ID, nls.localize('reverseContinue', "Reverse"), 60, { id: 'codicon/debug-reverse-continue' }, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + registry.registerWorkbenchAction(SyncActionDescriptor.from(ConfigureAction), 'Debug: Open launch.json', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(AddFunctionBreakpointAction), 'Debug: Add Function Breakpoint', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(ReapplyBreakpointsAction), 'Debug: Reapply All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(RemoveAllBreakpointsAction), 'Debug: Remove All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(EnableAllBreakpointsAction), 'Debug: Enable All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(DisableAllBreakpointsAction), 'Debug: Disable All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(SelectAndStartAction), 'Debug: Select and Start Debugging', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(ClearReplAction), 'Debug: Clear Console', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); -// Debug callstack context menu -const registerDebugCallstackItem = (id: string, title: string, order: number, when?: ContextKeyExpression, precondition?: ContextKeyExpression, group = 'navigation') => { - MenuRegistry.appendMenuItem(MenuId.DebugCallStackContext, { - group, - when, - order, - command: { - id, - title, - precondition - } - }); -}; -registerDebugCallstackItem(RESTART_SESSION_ID, RESTART_LABEL, 10, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session')); -registerDebugCallstackItem(STOP_ID, STOP_LABEL, 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session')); -registerDebugCallstackItem(PAUSE_ID, PAUSE_LABEL, 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('running'))); -registerDebugCallstackItem(CONTINUE_ID, CONTINUE_LABEL, 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped'))); -registerDebugCallstackItem(STEP_OVER_ID, STEP_OVER_LABEL, 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugCallstackItem(STEP_INTO_ID, STEP_INTO_LABEL, 30, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugCallstackItem(STEP_OUT_ID, STEP_OUT_LABEL, 40, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugCallstackItem(TERMINATE_THREAD_ID, nls.localize('terminateThread', "Terminate Thread"), 10, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), undefined, 'termination'); -registerDebugCallstackItem(RESTART_FRAME_ID, nls.localize('restartFrame', "Restart Frame"), 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame'), CONTEXT_RESTART_FRAME_SUPPORTED)); -registerDebugCallstackItem(COPY_STACK_TRACE_ID, nls.localize('copyStackTrace', "Copy Call Stack"), 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame')); - -// Editor contributions - -registerEditorContribution('editor.contrib.callStack', CallStackEditorContribution); -registerEditorContribution(BREAKPOINT_EDITOR_CONTRIBUTION_ID, BreakpointEditorContribution); - -// View menu - -MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '3_views', - command: { - id: VIEWLET_ID, - title: nls.localize({ key: 'miViewRun', comment: ['&& denotes a mnemonic'] }, "&&Run") - }, - order: 4 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '4_panels', - command: { - id: OpenDebugConsoleAction.ID, - title: nls.localize({ key: 'miToggleDebugConsole', comment: ['&& denotes a mnemonic'] }, "De&&bug Console") - }, - order: 2 -}); - -// Debug menu - -MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '1_debug', - command: { - id: StartAction.ID, - title: nls.localize({ key: 'miStartDebugging', comment: ['&& denotes a mnemonic'] }, "&&Start Debugging") - }, - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '1_debug', - command: { - id: RunAction.ID, - title: nls.localize({ key: 'miRun', comment: ['&& denotes a mnemonic'] }, "Run &&Without Debugging") - }, - order: 2 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '1_debug', - command: { - id: STOP_ID, - title: nls.localize({ key: 'miStopDebugging', comment: ['&& denotes a mnemonic'] }, "&&Stop Debugging"), - precondition: CONTEXT_IN_DEBUG_MODE - }, - order: 3 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '1_debug', - command: { - id: RESTART_SESSION_ID, - title: nls.localize({ key: 'miRestart Debugging', comment: ['&& denotes a mnemonic'] }, "&&Restart Debugging"), - precondition: CONTEXT_IN_DEBUG_MODE - }, - order: 4 -}); - -// Configuration -MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '2_configuration', - command: { - id: ConfigureAction.ID, - title: nls.localize({ key: 'miOpenConfigurations', comment: ['&& denotes a mnemonic'] }, "Open &&Configurations") - }, - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '2_configuration', - command: { - id: ADD_CONFIGURATION_ID, - title: nls.localize({ key: 'miAddConfiguration', comment: ['&& denotes a mnemonic'] }, "A&&dd Configuration...") - }, - order: 2 -}); - -// Step Commands -MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '3_step', - command: { - id: STEP_OVER_ID, - title: nls.localize({ key: 'miStepOver', comment: ['&& denotes a mnemonic'] }, "Step &&Over"), - precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') - }, - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '3_step', - command: { - id: STEP_INTO_ID, - title: nls.localize({ key: 'miStepInto', comment: ['&& denotes a mnemonic'] }, "Step &&Into"), - precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') - }, - order: 2 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '3_step', - command: { - id: STEP_OUT_ID, - title: nls.localize({ key: 'miStepOut', comment: ['&& denotes a mnemonic'] }, "Step O&&ut"), - precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') - }, - order: 3 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '3_step', - command: { - id: CONTINUE_ID, - title: nls.localize({ key: 'miContinue', comment: ['&& denotes a mnemonic'] }, "&&Continue"), - precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') - }, - order: 4 -}); - -// New Breakpoints -MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '4_new_breakpoint', - command: { - id: TOGGLE_BREAKPOINT_ID, - title: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint") - }, - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { - group: '1_breakpoints', - command: { - id: TOGGLE_CONDITIONAL_BREAKPOINT_ID, - title: nls.localize({ key: 'miConditionalBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Conditional Breakpoint...") - }, - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { - group: '1_breakpoints', - command: { - id: TOGGLE_INLINE_BREAKPOINT_ID, - title: nls.localize({ key: 'miInlineBreakpoint', comment: ['&& denotes a mnemonic'] }, "Inline Breakp&&oint") - }, - order: 2 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { - group: '1_breakpoints', - command: { - id: AddFunctionBreakpointAction.ID, - title: nls.localize({ key: 'miFunctionBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Function Breakpoint...") - }, - order: 3 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { - group: '1_breakpoints', - command: { - id: ADD_LOG_POINT_ID, - title: nls.localize({ key: 'miLogPoint', comment: ['&& denotes a mnemonic'] }, "&&Logpoint...") - }, - order: 4 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '4_new_breakpoint', - title: nls.localize({ key: 'miNewBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&New Breakpoint"), - submenu: MenuId.MenubarNewBreakpointMenu, - order: 2 -}); - -// Modify Breakpoints -MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '5_breakpoints', - command: { - id: EnableAllBreakpointsAction.ID, - title: nls.localize({ key: 'miEnableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "&&Enable All Breakpoints") - }, - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '5_breakpoints', - command: { - id: DisableAllBreakpointsAction.ID, - title: nls.localize({ key: 'miDisableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Disable A&&ll Breakpoints") - }, - order: 2 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '5_breakpoints', - command: { - id: RemoveAllBreakpointsAction.ID, - title: nls.localize({ key: 'miRemoveAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Remove &&All Breakpoints") - }, - order: 3 -}); - -// Install Debuggers -MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: 'z_install', - command: { - id: 'debug.installAdditionalDebuggers', - title: nls.localize({ key: 'miInstallAdditionalDebuggers', comment: ['&& denotes a mnemonic'] }, "&&Install Additional Debuggers...") - }, - order: 1 -}); - -// Touch Bar -if (isMacintosh) { - - const registerTouchBarEntry = (id: string, title: string, order: number, when: ContextKeyExpression | undefined, iconUri: URI) => { - MenuRegistry.appendMenuItem(MenuId.TouchBarContext, { + const registerDebugCommandPaletteItem = (id: string, title: string, when?: ContextKeyExpression, precondition?: ContextKeyExpression) => { + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, when), command: { id, - title, - icon: { dark: iconUri } - }, - when, - group: '9_debug', - order + title: `Debug: ${title}`, + precondition + } }); }; - registerTouchBarEntry(StartAction.ID, StartAction.LABEL, 0, CONTEXT_IN_DEBUG_MODE.toNegated(), URI.parse(require.toUrl('vs/workbench/contrib/debug/browser/media/continue-tb.png'))); - registerTouchBarEntry(RunAction.ID, RunAction.LABEL, 1, CONTEXT_IN_DEBUG_MODE.toNegated(), URI.parse(require.toUrl('vs/workbench/contrib/debug/browser/media/continue-without-debugging-tb.png'))); - registerTouchBarEntry(CONTINUE_ID, CONTINUE_LABEL, 0, CONTEXT_DEBUG_STATE.isEqualTo('stopped'), URI.parse(require.toUrl('vs/workbench/contrib/debug/browser/media/continue-tb.png'))); - registerTouchBarEntry(PAUSE_ID, PAUSE_LABEL, 1, ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, ContextKeyExpr.notEquals('debugState', 'stopped')), URI.parse(require.toUrl('vs/workbench/contrib/debug/browser/media/pause-tb.png'))); - registerTouchBarEntry(STEP_OVER_ID, STEP_OVER_LABEL, 2, CONTEXT_IN_DEBUG_MODE, URI.parse(require.toUrl('vs/workbench/contrib/debug/browser/media/stepover-tb.png'))); - registerTouchBarEntry(STEP_INTO_ID, STEP_INTO_LABEL, 3, CONTEXT_IN_DEBUG_MODE, URI.parse(require.toUrl('vs/workbench/contrib/debug/browser/media/stepinto-tb.png'))); - registerTouchBarEntry(STEP_OUT_ID, STEP_OUT_LABEL, 4, CONTEXT_IN_DEBUG_MODE, URI.parse(require.toUrl('vs/workbench/contrib/debug/browser/media/stepout-tb.png'))); - registerTouchBarEntry(RESTART_SESSION_ID, RESTART_LABEL, 5, CONTEXT_IN_DEBUG_MODE, URI.parse(require.toUrl('vs/workbench/contrib/debug/browser/media/restart-tb.png'))); - registerTouchBarEntry(STOP_ID, STOP_LABEL, 6, CONTEXT_IN_DEBUG_MODE, URI.parse(require.toUrl('vs/workbench/contrib/debug/browser/media/stop-tb.png'))); + registerDebugCommandPaletteItem(RESTART_SESSION_ID, RESTART_LABEL); + registerDebugCommandPaletteItem(TERMINATE_THREAD_ID, nls.localize('terminateThread', "Terminate Thread"), CONTEXT_IN_DEBUG_MODE); + registerDebugCommandPaletteItem(STEP_OVER_ID, STEP_OVER_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + registerDebugCommandPaletteItem(STEP_INTO_ID, STEP_INTO_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + registerDebugCommandPaletteItem(STEP_OUT_ID, STEP_OUT_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + registerDebugCommandPaletteItem(PAUSE_ID, PAUSE_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('running')); + registerDebugCommandPaletteItem(DISCONNECT_ID, DISCONNECT_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_FOCUSED_SESSION_IS_ATTACH); + registerDebugCommandPaletteItem(STOP_ID, STOP_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated()); + registerDebugCommandPaletteItem(CONTINUE_ID, CONTINUE_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + registerDebugCommandPaletteItem(FOCUS_REPL_ID, nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugFocusConsole' }, 'Focus on Debug Console View')); + registerDebugCommandPaletteItem(JUMP_TO_CURSOR_ID, nls.localize('jumpToCursor', "Jump to Cursor"), CONTEXT_JUMP_TO_CURSOR_SUPPORTED); + registerDebugCommandPaletteItem(JUMP_TO_CURSOR_ID, nls.localize('SetNextStatement', "Set Next Statement"), CONTEXT_JUMP_TO_CURSOR_SUPPORTED); + registerDebugCommandPaletteItem(RunToCursorAction.ID, RunToCursorAction.LABEL, ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped'))); + registerDebugCommandPaletteItem(TOGGLE_INLINE_BREAKPOINT_ID, nls.localize('inlineBreakpoint', "Inline Breakpoint")); + + // Debug toolbar + + const registerDebugToolBarItem = (id: string, title: string, order: number, icon: { light?: URI, dark?: URI } | ThemeIcon, when?: ContextKeyExpression, precondition?: ContextKeyExpression) => { + MenuRegistry.appendMenuItem(MenuId.DebugToolBar, { + group: 'navigation', + when, + order, + command: { + id, + title, + icon, + precondition + } + }); + }; + + registerDebugToolBarItem(CONTINUE_ID, CONTINUE_LABEL, 10, { id: 'codicon/debug-continue' }, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + registerDebugToolBarItem(PAUSE_ID, PAUSE_LABEL, 10, { id: 'codicon/debug-pause' }, CONTEXT_DEBUG_STATE.notEqualsTo('stopped'), CONTEXT_DEBUG_STATE.isEqualTo('running')); + registerDebugToolBarItem(STOP_ID, STOP_LABEL, 70, { id: 'codicon/debug-stop' }, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated()); + registerDebugToolBarItem(DISCONNECT_ID, DISCONNECT_LABEL, 70, { id: 'codicon/debug-disconnect' }, CONTEXT_FOCUSED_SESSION_IS_ATTACH); + registerDebugToolBarItem(STEP_OVER_ID, STEP_OVER_LABEL, 20, { id: 'codicon/debug-step-over' }, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + registerDebugToolBarItem(STEP_INTO_ID, STEP_INTO_LABEL, 30, { id: 'codicon/debug-step-into' }, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + registerDebugToolBarItem(STEP_OUT_ID, STEP_OUT_LABEL, 40, { id: 'codicon/debug-step-out' }, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + registerDebugToolBarItem(RESTART_SESSION_ID, RESTART_LABEL, 60, { id: 'codicon/debug-restart' }); + registerDebugToolBarItem(STEP_BACK_ID, nls.localize('stepBackDebug', "Step Back"), 50, { id: 'codicon/debug-step-back' }, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + registerDebugToolBarItem(REVERSE_CONTINUE_ID, nls.localize('reverseContinue', "Reverse"), 60, { id: 'codicon/debug-reverse-continue' }, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + + // Debug callstack context menu + const registerDebugCallstackItem = (id: string, title: string, order: number, when?: ContextKeyExpression, precondition?: ContextKeyExpression, group = 'navigation') => { + MenuRegistry.appendMenuItem(MenuId.DebugCallStackContext, { + group, + when, + order, + command: { + id, + title, + precondition + } + }); + }; + registerDebugCallstackItem(RESTART_SESSION_ID, RESTART_LABEL, 10, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session')); + registerDebugCallstackItem(STOP_ID, STOP_LABEL, 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session')); + registerDebugCallstackItem(PAUSE_ID, PAUSE_LABEL, 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('running'))); + registerDebugCallstackItem(CONTINUE_ID, CONTINUE_LABEL, 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped'))); + registerDebugCallstackItem(STEP_OVER_ID, STEP_OVER_LABEL, 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + registerDebugCallstackItem(STEP_INTO_ID, STEP_INTO_LABEL, 30, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + registerDebugCallstackItem(STEP_OUT_ID, STEP_OUT_LABEL, 40, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + registerDebugCallstackItem(TERMINATE_THREAD_ID, nls.localize('terminateThread', "Terminate Thread"), 10, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), undefined, 'termination'); + registerDebugCallstackItem(RESTART_FRAME_ID, nls.localize('restartFrame', "Restart Frame"), 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame'), CONTEXT_RESTART_FRAME_SUPPORTED)); + registerDebugCallstackItem(COPY_STACK_TRACE_ID, nls.localize('copyStackTrace', "Copy Call Stack"), 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame')); + + // Touch Bar + if (isMacintosh) { + + const registerTouchBarEntry = (id: string, title: string, order: number, when: ContextKeyExpression | undefined, iconUri: URI) => { + MenuRegistry.appendMenuItem(MenuId.TouchBarContext, { + command: { + id, + title, + icon: { dark: iconUri } + }, + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, when), + group: '9_debug', + order + }); + }; + + registerTouchBarEntry(StartAction.ID, StartAction.LABEL, 0, CONTEXT_IN_DEBUG_MODE.toNegated(), URI.parse(require.toUrl('vs/workbench/contrib/debug/browser/media/continue-tb.png'))); + registerTouchBarEntry(RunAction.ID, RunAction.LABEL, 1, CONTEXT_IN_DEBUG_MODE.toNegated(), URI.parse(require.toUrl('vs/workbench/contrib/debug/browser/media/continue-without-debugging-tb.png'))); + registerTouchBarEntry(CONTINUE_ID, CONTINUE_LABEL, 0, CONTEXT_DEBUG_STATE.isEqualTo('stopped'), URI.parse(require.toUrl('vs/workbench/contrib/debug/browser/media/continue-tb.png'))); + registerTouchBarEntry(PAUSE_ID, PAUSE_LABEL, 1, ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, ContextKeyExpr.notEquals('debugState', 'stopped')), URI.parse(require.toUrl('vs/workbench/contrib/debug/browser/media/pause-tb.png'))); + registerTouchBarEntry(STEP_OVER_ID, STEP_OVER_LABEL, 2, CONTEXT_IN_DEBUG_MODE, URI.parse(require.toUrl('vs/workbench/contrib/debug/browser/media/stepover-tb.png'))); + registerTouchBarEntry(STEP_INTO_ID, STEP_INTO_LABEL, 3, CONTEXT_IN_DEBUG_MODE, URI.parse(require.toUrl('vs/workbench/contrib/debug/browser/media/stepinto-tb.png'))); + registerTouchBarEntry(STEP_OUT_ID, STEP_OUT_LABEL, 4, CONTEXT_IN_DEBUG_MODE, URI.parse(require.toUrl('vs/workbench/contrib/debug/browser/media/stepout-tb.png'))); + registerTouchBarEntry(RESTART_SESSION_ID, RESTART_LABEL, 5, CONTEXT_IN_DEBUG_MODE, URI.parse(require.toUrl('vs/workbench/contrib/debug/browser/media/restart-tb.png'))); + registerTouchBarEntry(STOP_ID, STOP_LABEL, 6, CONTEXT_IN_DEBUG_MODE, URI.parse(require.toUrl('vs/workbench/contrib/debug/browser/media/stop-tb.png'))); + } } -// Color contributions +function registerDebugMenu(): void { + // View menu -const debugTokenExpressionName = registerColor('debugTokenExpression.name', { dark: '#c586c0', light: '#9b46b0', hc: foreground }, 'Foreground color for the token names shown in the debug views (ie. the Variables or Watch view).'); -const debugTokenExpressionValue = registerColor('debugTokenExpression.value', { dark: '#cccccc99', light: '#6c6c6ccc', hc: foreground }, 'Foreground color for the token values shown in the debug views (ie. the Variables or Watch view).'); -const debugTokenExpressionString = registerColor('debugTokenExpression.string', { dark: '#ce9178', light: '#a31515', hc: '#f48771' }, 'Foreground color for strings in the debug views (ie. the Variables or Watch view).'); -const debugTokenExpressionBoolean = registerColor('debugTokenExpression.boolean', { dark: '#4e94ce', light: '#0000ff', hc: '#75bdfe' }, 'Foreground color for booleans in the debug views (ie. the Variables or Watch view).'); -const debugTokenExpressionNumber = registerColor('debugTokenExpression.number', { dark: '#b5cea8', light: '#098658', hc: '#89d185' }, 'Foreground color for numbers in the debug views (ie. the Variables or Watch view).'); -const debugTokenExpressionError = registerColor('debugTokenExpression.error', { dark: '#f48771', light: '#e51400', hc: '#f48771' }, 'Foreground color for expression errors in the debug views (ie. the Variables or Watch view) and for error logs shown in the debug console.'); + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '3_views', + command: { + id: VIEWLET_ID, + title: nls.localize({ key: 'miViewRun', comment: ['&& denotes a mnemonic'] }, "&&Run") + }, + order: 4 + }); -const debugViewExceptionLabelForeground = registerColor('debugView.exceptionLabelForeground', { dark: foreground, light: '#FFF', hc: foreground }, 'Foreground color for a label shown in the CALL STACK view when the debugger breaks on an exception.'); -const debugViewExceptionLabelBackground = registerColor('debugView.exceptionLabelBackground', { dark: '#6C2022', light: '#A31515', hc: '#6C2022' }, 'Background color for a label shown in the CALL STACK view when the debugger breaks on an exception.'); -const debugViewStateLabelForeground = registerColor('debugView.stateLabelForeground', { dark: foreground, light: foreground, hc: foreground }, 'Foreground color for a label in the CALL STACK view showing the current session\'s or thread\'s state.'); -const debugViewStateLabelBackground = registerColor('debugView.stateLabelBackground', { dark: '#88888844', light: '#88888844', hc: '#88888844' }, 'Background color for a label in the CALL STACK view showing the current session\'s or thread\'s state.'); -const debugViewValueChangedHighlight = registerColor('debugView.valueChangedHighlight', { dark: '#569CD6', light: '#569CD6', hc: '#569CD6' }, 'Color used to highlight value changes in the debug views (ie. in the Variables view).'); + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '4_panels', + command: { + id: OpenDebugConsoleAction.ID, + title: nls.localize({ key: 'miToggleDebugConsole', comment: ['&& denotes a mnemonic'] }, "De&&bug Console") + }, + order: 2 + }); -const debugConsoleInfoForeground = registerColor('debugConsole.infoForeground', { dark: editorInfoForeground, light: editorInfoForeground, hc: foreground }, 'Foreground color for info messages in debug REPL console.'); -const debugConsoleWarningForeground = registerColor('debugConsole.warningForeground', { dark: editorWarningForeground, light: editorWarningForeground, hc: '#008000' }, 'Foreground color for warning messages in debug REPL console.'); -const debugConsoleErrorForeground = registerColor('debugConsole.errorForeground', { dark: errorForeground, light: errorForeground, hc: errorForeground }, 'Foreground color for error messages in debug REPL console.'); -const debugConsoleSourceForeground = registerColor('debugConsole.sourceForeground', { dark: foreground, light: foreground, hc: foreground }, 'Foreground color for source filenames in debug REPL console.'); -const debugConsoleInputIconForeground = registerColor('debugConsoleInputIcon.foreground', { dark: foreground, light: foreground, hc: foreground }, 'Foreground color for debug console input marker icon.'); + // Debug menu -registerThemingParticipant((theme, collector) => { - // All these colours provide a default value so they will never be undefined, hence the `!` - const badgeBackgroundColor = theme.getColor(badgeBackground)!; - const badgeForegroundColor = theme.getColor(badgeForeground)!; - const listDeemphasizedForegroundColor = theme.getColor(listDeemphasizedForeground)!; - const debugViewExceptionLabelForegroundColor = theme.getColor(debugViewExceptionLabelForeground)!; - const debugViewExceptionLabelBackgroundColor = theme.getColor(debugViewExceptionLabelBackground)!; - const debugViewStateLabelForegroundColor = theme.getColor(debugViewStateLabelForeground)!; - const debugViewStateLabelBackgroundColor = theme.getColor(debugViewStateLabelBackground)!; - const debugViewValueChangedHighlightColor = theme.getColor(debugViewValueChangedHighlight)!; + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '1_debug', + command: { + id: StartAction.ID, + title: nls.localize({ key: 'miStartDebugging', comment: ['&& denotes a mnemonic'] }, "&&Start Debugging") + }, + order: 1 + }); - collector.addRule(` - /* Text colour of the call stack row's filename */ - .debug-pane .debug-call-stack .monaco-list-row:not(.selected) .stack-frame > .file .file-name { - color: ${listDeemphasizedForegroundColor} - } + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '1_debug', + command: { + id: RunAction.ID, + title: nls.localize({ key: 'miRun', comment: ['&& denotes a mnemonic'] }, "Run &&Without Debugging") + }, + order: 2 + }); - /* Line & column number "badge" for selected call stack row */ - .debug-pane .monaco-list-row.selected .line-number { - background-color: ${badgeBackgroundColor}; - color: ${badgeForegroundColor}; - } + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '1_debug', + command: { + id: STOP_ID, + title: nls.localize({ key: 'miStopDebugging', comment: ['&& denotes a mnemonic'] }, "&&Stop Debugging"), + precondition: CONTEXT_IN_DEBUG_MODE + }, + order: 3 + }); - /* Line & column number "badge" for unselected call stack row (basically all other rows) */ - .debug-pane .line-number { - background-color: ${badgeBackgroundColor.transparent(0.6)}; - color: ${badgeForegroundColor.transparent(0.6)}; - } + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '1_debug', + command: { + id: RESTART_SESSION_ID, + title: nls.localize({ key: 'miRestart Debugging', comment: ['&& denotes a mnemonic'] }, "&&Restart Debugging"), + precondition: CONTEXT_IN_DEBUG_MODE + }, + order: 4 + }); - /* State "badge" displaying the active session's current state. - * Only visible when there are more active debug sessions/threads running. - */ - .debug-pane .debug-call-stack .thread > .state.label, - .debug-pane .debug-call-stack .session > .state.label, - .debug-pane .monaco-list-row.selected .thread > .state.label, - .debug-pane .monaco-list-row.selected .session > .state.label { - background-color: ${debugViewStateLabelBackgroundColor}; - color: ${debugViewStateLabelForegroundColor}; - } + // Configuration + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '2_configuration', + command: { + id: ConfigureAction.ID, + title: nls.localize({ key: 'miOpenConfigurations', comment: ['&& denotes a mnemonic'] }, "Open &&Configurations") + }, + order: 1 + }); - /* Info "badge" shown when the debugger pauses due to a thrown exception. */ - .debug-pane .debug-call-stack-title > .pause-message > .label.exception { - background-color: ${debugViewExceptionLabelBackgroundColor}; - color: ${debugViewExceptionLabelForegroundColor}; - } + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '2_configuration', + command: { + id: ADD_CONFIGURATION_ID, + title: nls.localize({ key: 'miAddConfiguration', comment: ['&& denotes a mnemonic'] }, "A&&dd Configuration...") + }, + order: 2 + }); - /* Animation of changed values in Debug viewlet */ - @keyframes debugViewletValueChanged { - 0% { background-color: ${debugViewValueChangedHighlightColor.transparent(0)} } - 5% { background-color: ${debugViewValueChangedHighlightColor.transparent(0.9)} } - 100% { background-color: ${debugViewValueChangedHighlightColor.transparent(0.3)} } - } + // Step Commands + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '3_step', + command: { + id: STEP_OVER_ID, + title: nls.localize({ key: 'miStepOver', comment: ['&& denotes a mnemonic'] }, "Step &&Over"), + precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') + }, + order: 1 + }); - .debug-pane .monaco-list-row .expression .value.changed { - background-color: ${debugViewValueChangedHighlightColor.transparent(0.3)}; - animation-name: debugViewletValueChanged; - animation-duration: 1s; - animation-fill-mode: forwards; - } - `); + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '3_step', + command: { + id: STEP_INTO_ID, + title: nls.localize({ key: 'miStepInto', comment: ['&& denotes a mnemonic'] }, "Step &&Into"), + precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') + }, + order: 2 + }); - const contrastBorderColor = theme.getColor(contrastBorder); + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '3_step', + command: { + id: STEP_OUT_ID, + title: nls.localize({ key: 'miStepOut', comment: ['&& denotes a mnemonic'] }, "Step O&&ut"), + precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') + }, + order: 3 + }); - if (contrastBorderColor) { - collector.addRule(` - .debug-pane .line-number { - border: 1px solid ${contrastBorderColor}; - } - `); - } + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '3_step', + command: { + id: CONTINUE_ID, + title: nls.localize({ key: 'miContinue', comment: ['&& denotes a mnemonic'] }, "&&Continue"), + precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') + }, + order: 4 + }); - const tokenNameColor = theme.getColor(debugTokenExpressionName)!; - const tokenValueColor = theme.getColor(debugTokenExpressionValue)!; - const tokenStringColor = theme.getColor(debugTokenExpressionString)!; - const tokenBooleanColor = theme.getColor(debugTokenExpressionBoolean)!; - const tokenErrorColor = theme.getColor(debugTokenExpressionError)!; - const tokenNumberColor = theme.getColor(debugTokenExpressionNumber)!; + // New Breakpoints + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '4_new_breakpoint', + command: { + id: TOGGLE_BREAKPOINT_ID, + title: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint") + }, + order: 1 + }); - collector.addRule(` - .monaco-workbench .monaco-list-row .expression .name { - color: ${tokenNameColor}; - } + MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { + group: '1_breakpoints', + command: { + id: TOGGLE_CONDITIONAL_BREAKPOINT_ID, + title: nls.localize({ key: 'miConditionalBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Conditional Breakpoint...") + }, + order: 1 + }); - .monaco-workbench .monaco-list-row .expression .value, - .monaco-workbench .debug-hover-widget .value { - color: ${tokenValueColor}; - } + MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { + group: '1_breakpoints', + command: { + id: TOGGLE_INLINE_BREAKPOINT_ID, + title: nls.localize({ key: 'miInlineBreakpoint', comment: ['&& denotes a mnemonic'] }, "Inline Breakp&&oint") + }, + order: 2 + }); - .monaco-workbench .monaco-list-row .expression .value.string, - .monaco-workbench .debug-hover-widget .value.string { - color: ${tokenStringColor}; - } + MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { + group: '1_breakpoints', + command: { + id: AddFunctionBreakpointAction.ID, + title: nls.localize({ key: 'miFunctionBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Function Breakpoint...") + }, + order: 3 + }); - .monaco-workbench .monaco-list-row .expression .value.boolean, - .monaco-workbench .debug-hover-widget .value.boolean { - color: ${tokenBooleanColor}; - } + MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { + group: '1_breakpoints', + command: { + id: ADD_LOG_POINT_ID, + title: nls.localize({ key: 'miLogPoint', comment: ['&& denotes a mnemonic'] }, "&&Logpoint...") + }, + order: 4 + }); - .monaco-workbench .monaco-list-row .expression .error, - .monaco-workbench .debug-hover-widget .error, - .monaco-workbench .debug-pane .debug-variables .scope .error { - color: ${tokenErrorColor}; - } + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '4_new_breakpoint', + title: nls.localize({ key: 'miNewBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&New Breakpoint"), + submenu: MenuId.MenubarNewBreakpointMenu, + order: 2 + }); - .monaco-workbench .monaco-list-row .expression .value.number, - .monaco-workbench .debug-hover-widget .value.number { - color: ${tokenNumberColor}; - } - `); + // Modify Breakpoints + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '5_breakpoints', + command: { + id: EnableAllBreakpointsAction.ID, + title: nls.localize({ key: 'miEnableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "&&Enable All Breakpoints") + }, + order: 1 + }); - const debugConsoleInputBorderColor = theme.getColor(inputBorder) || Color.fromHex('#80808060'); - const debugConsoleInfoForegroundColor = theme.getColor(debugConsoleInfoForeground)!; - const debugConsoleWarningForegroundColor = theme.getColor(debugConsoleWarningForeground)!; - const debugConsoleErrorForegroundColor = theme.getColor(debugConsoleErrorForeground)!; - const debugConsoleSourceForegroundColor = theme.getColor(debugConsoleSourceForeground)!; - const debugConsoleInputIconForegroundColor = theme.getColor(debugConsoleInputIconForeground)!; + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '5_breakpoints', + command: { + id: DisableAllBreakpointsAction.ID, + title: nls.localize({ key: 'miDisableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Disable A&&ll Breakpoints") + }, + order: 2 + }); - collector.addRule(` - .repl .repl-input-wrapper { - border-top: 1px solid ${debugConsoleInputBorderColor}; - } + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '5_breakpoints', + command: { + id: RemoveAllBreakpointsAction.ID, + title: nls.localize({ key: 'miRemoveAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Remove &&All Breakpoints") + }, + order: 3 + }); - .monaco-workbench .repl .repl-tree .output .expression .value.info { - color: ${debugConsoleInfoForegroundColor}; - } + // Install Debuggers + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: 'z_install', + command: { + id: 'debug.installAdditionalDebuggers', + title: nls.localize({ key: 'miInstallAdditionalDebuggers', comment: ['&& denotes a mnemonic'] }, "&&Install Additional Debuggers...") + }, + order: 1 + }); +} - .monaco-workbench .repl .repl-tree .output .expression .value.warn { - color: ${debugConsoleWarningForegroundColor}; - } +function registerDebugPanel(): void { + // register repl panel - .monaco-workbench .repl .repl-tree .output .expression .value.error { - color: ${debugConsoleErrorForegroundColor}; - } + const VIEW_CONTAINER: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ + id: DEBUG_PANEL_ID, + name: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugPanel' }, 'Debug Console'), + icon: Codicon.debugConsole.classNames, + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [DEBUG_PANEL_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), + storageId: DEBUG_PANEL_ID, + focusCommand: { id: OpenDebugConsoleAction.ID }, + order: 2, + hideIfEmpty: true + }, ViewContainerLocation.Panel); - .monaco-workbench .repl .repl-tree .output .expression .source { - color: ${debugConsoleSourceForegroundColor}; - } + Registry.as(ViewExtensions.ViewsRegistry).registerViews([{ + id: REPL_VIEW_ID, + name: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugPanel' }, 'Debug Console'), + containerIcon: Codicon.debugConsole.classNames, + canToggleVisibility: false, + canMoveView: true, + when: CONTEXT_DEBUGGERS_AVAILABLE, + ctorDescriptor: new SyncDescriptor(Repl), + }], VIEW_CONTAINER); - .monaco-workbench .repl .repl-tree .monaco-tl-contents .arrow { - color: ${debugConsoleInputIconForegroundColor}; - } - `); + registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenDebugConsoleAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Y }), 'View: Debug Console', nls.localize('view', "View"), CONTEXT_DEBUGGERS_AVAILABLE); +} - if (!theme.defines(debugConsoleInputIconForeground)) { - collector.addRule(` - .monaco-workbench.vs .repl .repl-tree .monaco-tl-contents .arrow { - opacity: 0.25; +function registerDebugView(): void { + const viewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ + id: VIEWLET_ID, + name: nls.localize('run', "Run"), + ctorDescriptor: new SyncDescriptor(DebugViewPaneContainer), + icon: Codicon.debugAlt.classNames, + alwaysUseContainerInfo: true, + order: 2 + }, ViewContainerLocation.Sidebar); + registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenDebugViewletAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_D }), 'View: Show Run and Debug', nls.localize('view', "View")); + + // Register default debug views + const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); + viewsRegistry.registerViews([{ id: VARIABLES_VIEW_ID, name: nls.localize('variables', "Variables"), containerIcon: Codicon.debugAlt.classNames, ctorDescriptor: new SyncDescriptor(VariablesView), order: 10, weight: 40, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusVariablesView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); + viewsRegistry.registerViews([{ id: WATCH_VIEW_ID, name: nls.localize('watch', "Watch"), containerIcon: Codicon.debugAlt.classNames, ctorDescriptor: new SyncDescriptor(WatchExpressionsView), order: 20, weight: 10, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusWatchView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); + viewsRegistry.registerViews([{ id: CALLSTACK_VIEW_ID, name: nls.localize('callStack', "Call Stack"), containerIcon: Codicon.debugAlt.classNames, ctorDescriptor: new SyncDescriptor(CallStackView), order: 30, weight: 30, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusCallStackView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); + viewsRegistry.registerViews([{ id: BREAKPOINTS_VIEW_ID, name: nls.localize('breakpoints', "Breakpoints"), containerIcon: Codicon.debugAlt.classNames, ctorDescriptor: new SyncDescriptor(BreakpointsView), order: 40, weight: 20, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusBreakpointsView' }, when: ContextKeyExpr.or(CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_UX.isEqualTo('default')) }], viewContainer); + viewsRegistry.registerViews([{ id: WelcomeView.ID, name: WelcomeView.LABEL, containerIcon: Codicon.debugAlt.classNames, ctorDescriptor: new SyncDescriptor(WelcomeView), order: 1, weight: 40, canToggleVisibility: true, when: CONTEXT_DEBUG_UX.isEqualTo('simple') }], viewContainer); + viewsRegistry.registerViews([{ id: LOADED_SCRIPTS_VIEW_ID, name: nls.localize('loadedScripts', "Loaded Scripts"), containerIcon: Codicon.debugAlt.classNames, ctorDescriptor: new SyncDescriptor(LoadedScriptsView), order: 35, weight: 5, canToggleVisibility: true, canMoveView: true, collapsed: true, when: ContextKeyExpr.and(CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_DEBUG_UX.isEqualTo('default')) }], viewContainer); +} + +function registerConfiguration(): void { + // Register configuration + const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); + configurationRegistry.registerConfiguration({ + id: 'debug', + order: 20, + title: nls.localize('debugConfigurationTitle', "Debug"), + type: 'object', + properties: { + 'debug.allowBreakpointsEverywhere': { + type: 'boolean', + description: nls.localize({ comment: ['This is the description for a setting'], key: 'allowBreakpointsEverywhere' }, "Allow setting breakpoints in any file."), + default: false + }, + 'debug.openExplorerOnEnd': { + type: 'boolean', + description: nls.localize({ comment: ['This is the description for a setting'], key: 'openExplorerOnEnd' }, "Automatically open the explorer view at the end of a debug session."), + default: false + }, + 'debug.inlineValues': { + type: 'boolean', + description: nls.localize({ comment: ['This is the description for a setting'], key: 'inlineValues' }, "Show variable values inline in editor while debugging."), + default: false + }, + 'debug.toolBarLocation': { + enum: ['floating', 'docked', 'hidden'], + markdownDescription: nls.localize({ comment: ['This is the description for a setting'], key: 'toolBarLocation' }, "Controls the location of the debug toolbar. Either `floating` in all views, `docked` in the debug view, or `hidden`."), + default: 'floating' + }, + 'debug.showInStatusBar': { + enum: ['never', 'always', 'onFirstSessionStart'], + enumDescriptions: [nls.localize('never', "Never show debug in status bar"), nls.localize('always', "Always show debug in status bar"), nls.localize('onFirstSessionStart', "Show debug in status bar only after debug was started for the first time")], + description: nls.localize({ comment: ['This is the description for a setting'], key: 'showInStatusBar' }, "Controls when the debug status bar should be visible."), + default: 'onFirstSessionStart' + }, + 'debug.internalConsoleOptions': INTERNAL_CONSOLE_OPTIONS_SCHEMA, + 'debug.console.closeOnEnd': { + type: 'boolean', + description: nls.localize('debug.console.closeOnEnd', "Controls if the debug console should be automatically closed when the debug session ends."), + default: false + }, + 'debug.openDebug': { + enum: ['neverOpen', 'openOnSessionStart', 'openOnFirstSessionStart', 'openOnDebugBreak'], + default: 'openOnFirstSessionStart', + description: nls.localize('openDebug', "Controls when the debug view should open.") + }, + 'debug.showSubSessionsInToolBar': { + type: 'boolean', + description: nls.localize({ comment: ['This is the description for a setting'], key: 'showSubSessionsInToolBar' }, "Controls whether the debug sub-sessions are shown in the debug tool bar. When this setting is false the stop command on a sub-session will also stop the parent session."), + default: false + }, + 'debug.console.fontSize': { + type: 'number', + description: nls.localize('debug.console.fontSize', "Controls the font size in pixels in the debug console."), + default: isMacintosh ? 12 : 14, + }, + 'debug.console.fontFamily': { + type: 'string', + description: nls.localize('debug.console.fontFamily', "Controls the font family in the debug console."), + default: 'default' + }, + 'debug.console.lineHeight': { + type: 'number', + description: nls.localize('debug.console.lineHeight', "Controls the line height in pixels in the debug console. Use 0 to compute the line height from the font size."), + default: 0 + }, + 'debug.console.wordWrap': { + type: 'boolean', + description: nls.localize('debug.console.wordWrap', "Controls if the lines should wrap in the debug console."), + default: true + }, + 'debug.console.historySuggestions': { + type: 'boolean', + description: nls.localize('debug.console.historySuggestions', "Controls if the debug console should suggest previously typed input."), + default: true + }, + 'launch': { + type: 'object', + description: nls.localize({ comment: ['This is the description for a setting'], key: 'launch' }, "Global debug launch configuration. Should be used as an alternative to 'launch.json' that is shared across workspaces."), + default: { configurations: [], compounds: [] }, + $ref: launchSchemaId + }, + 'debug.focusWindowOnBreak': { + type: 'boolean', + description: nls.localize('debug.focusWindowOnBreak', "Controls whether the workbench window should be focused when the debugger breaks."), + default: true + }, + 'debug.onTaskErrors': { + enum: ['debugAnyway', 'showErrors', 'prompt', 'abort'], + enumDescriptions: [nls.localize('debugAnyway', "Ignore task errors and start debugging."), nls.localize('showErrors', "Show the Problems view and do not start debugging."), nls.localize('prompt', "Prompt user."), nls.localize('cancel', "Cancel debugging.")], + description: nls.localize('debug.onTaskErrors', "Controls what to do when errors are encountered after running a preLaunchTask."), + default: 'prompt' + }, + 'debug.showBreakpointsInOverviewRuler': { + type: 'boolean', + description: nls.localize({ comment: ['This is the description for a setting'], key: 'showBreakpointsInOverviewRuler' }, "Controls whether breakpoints should be shown in the overview ruler."), + default: false + }, + 'debug.showInlineBreakpointCandidates': { + type: 'boolean', + description: nls.localize({ comment: ['This is the description for a setting'], key: 'showInlineBreakpointCandidates' }, "Controls whether inline breakpoints candidate decorations should be shown in the editor while debugging."), + default: true } - - .monaco-workbench.vs-dark .repl .repl-tree .monaco-tl-contents .arrow { - opacity: 0.4; - } - - .monaco-workbench.hc-black .repl .repl-tree .monaco-tl-contents .arrow { - opacity: 1; - } - `); - } -}); + } + }); +} diff --git a/src/vs/workbench/contrib/debug/browser/debugColors.ts b/src/vs/workbench/contrib/debug/browser/debugColors.ts new file mode 100644 index 00000000000..df54038d02c --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/debugColors.ts @@ -0,0 +1,190 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerColor, foreground, editorInfoForeground, editorWarningForeground, errorForeground, badgeBackground, badgeForeground, listDeemphasizedForeground, contrastBorder, inputBorder } from 'vs/platform/theme/common/colorRegistry'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { Color } from 'vs/base/common/color'; + +export function registerColors() { + + const debugTokenExpressionName = registerColor('debugTokenExpression.name', { dark: '#c586c0', light: '#9b46b0', hc: foreground }, 'Foreground color for the token names shown in the debug views (ie. the Variables or Watch view).'); + const debugTokenExpressionValue = registerColor('debugTokenExpression.value', { dark: '#cccccc99', light: '#6c6c6ccc', hc: foreground }, 'Foreground color for the token values shown in the debug views (ie. the Variables or Watch view).'); + const debugTokenExpressionString = registerColor('debugTokenExpression.string', { dark: '#ce9178', light: '#a31515', hc: '#f48771' }, 'Foreground color for strings in the debug views (ie. the Variables or Watch view).'); + const debugTokenExpressionBoolean = registerColor('debugTokenExpression.boolean', { dark: '#4e94ce', light: '#0000ff', hc: '#75bdfe' }, 'Foreground color for booleans in the debug views (ie. the Variables or Watch view).'); + const debugTokenExpressionNumber = registerColor('debugTokenExpression.number', { dark: '#b5cea8', light: '#098658', hc: '#89d185' }, 'Foreground color for numbers in the debug views (ie. the Variables or Watch view).'); + const debugTokenExpressionError = registerColor('debugTokenExpression.error', { dark: '#f48771', light: '#e51400', hc: '#f48771' }, 'Foreground color for expression errors in the debug views (ie. the Variables or Watch view) and for error logs shown in the debug console.'); + + const debugViewExceptionLabelForeground = registerColor('debugView.exceptionLabelForeground', { dark: foreground, light: '#FFF', hc: foreground }, 'Foreground color for a label shown in the CALL STACK view when the debugger breaks on an exception.'); + const debugViewExceptionLabelBackground = registerColor('debugView.exceptionLabelBackground', { dark: '#6C2022', light: '#A31515', hc: '#6C2022' }, 'Background color for a label shown in the CALL STACK view when the debugger breaks on an exception.'); + const debugViewStateLabelForeground = registerColor('debugView.stateLabelForeground', { dark: foreground, light: foreground, hc: foreground }, 'Foreground color for a label in the CALL STACK view showing the current session\'s or thread\'s state.'); + const debugViewStateLabelBackground = registerColor('debugView.stateLabelBackground', { dark: '#88888844', light: '#88888844', hc: '#88888844' }, 'Background color for a label in the CALL STACK view showing the current session\'s or thread\'s state.'); + const debugViewValueChangedHighlight = registerColor('debugView.valueChangedHighlight', { dark: '#569CD6', light: '#569CD6', hc: '#569CD6' }, 'Color used to highlight value changes in the debug views (ie. in the Variables view).'); + + const debugConsoleInfoForeground = registerColor('debugConsole.infoForeground', { dark: editorInfoForeground, light: editorInfoForeground, hc: foreground }, 'Foreground color for info messages in debug REPL console.'); + const debugConsoleWarningForeground = registerColor('debugConsole.warningForeground', { dark: editorWarningForeground, light: editorWarningForeground, hc: '#008000' }, 'Foreground color for warning messages in debug REPL console.'); + const debugConsoleErrorForeground = registerColor('debugConsole.errorForeground', { dark: errorForeground, light: errorForeground, hc: errorForeground }, 'Foreground color for error messages in debug REPL console.'); + const debugConsoleSourceForeground = registerColor('debugConsole.sourceForeground', { dark: foreground, light: foreground, hc: foreground }, 'Foreground color for source filenames in debug REPL console.'); + const debugConsoleInputIconForeground = registerColor('debugConsoleInputIcon.foreground', { dark: foreground, light: foreground, hc: foreground }, 'Foreground color for debug console input marker icon.'); + + registerThemingParticipant((theme, collector) => { + // All these colours provide a default value so they will never be undefined, hence the `!` + const badgeBackgroundColor = theme.getColor(badgeBackground)!; + const badgeForegroundColor = theme.getColor(badgeForeground)!; + const listDeemphasizedForegroundColor = theme.getColor(listDeemphasizedForeground)!; + const debugViewExceptionLabelForegroundColor = theme.getColor(debugViewExceptionLabelForeground)!; + const debugViewExceptionLabelBackgroundColor = theme.getColor(debugViewExceptionLabelBackground)!; + const debugViewStateLabelForegroundColor = theme.getColor(debugViewStateLabelForeground)!; + const debugViewStateLabelBackgroundColor = theme.getColor(debugViewStateLabelBackground)!; + const debugViewValueChangedHighlightColor = theme.getColor(debugViewValueChangedHighlight)!; + + collector.addRule(` + /* Text colour of the call stack row's filename */ + .debug-pane .debug-call-stack .monaco-list-row:not(.selected) .stack-frame > .file .file-name { + color: ${listDeemphasizedForegroundColor} + } + + /* Line & column number "badge" for selected call stack row */ + .debug-pane .monaco-list-row.selected .line-number { + background-color: ${badgeBackgroundColor}; + color: ${badgeForegroundColor}; + } + + /* Line & column number "badge" for unselected call stack row (basically all other rows) */ + .debug-pane .line-number { + background-color: ${badgeBackgroundColor.transparent(0.6)}; + color: ${badgeForegroundColor.transparent(0.6)}; + } + + /* State "badge" displaying the active session's current state. + * Only visible when there are more active debug sessions/threads running. + */ + .debug-pane .debug-call-stack .thread > .state.label, + .debug-pane .debug-call-stack .session > .state.label, + .debug-pane .monaco-list-row.selected .thread > .state.label, + .debug-pane .monaco-list-row.selected .session > .state.label { + background-color: ${debugViewStateLabelBackgroundColor}; + color: ${debugViewStateLabelForegroundColor}; + } + + /* Info "badge" shown when the debugger pauses due to a thrown exception. */ + .debug-pane .debug-call-stack-title > .pause-message > .label.exception { + background-color: ${debugViewExceptionLabelBackgroundColor}; + color: ${debugViewExceptionLabelForegroundColor}; + } + + /* Animation of changed values in Debug viewlet */ + @keyframes debugViewletValueChanged { + 0% { background-color: ${debugViewValueChangedHighlightColor.transparent(0)} } + 5% { background-color: ${debugViewValueChangedHighlightColor.transparent(0.9)} } + 100% { background-color: ${debugViewValueChangedHighlightColor.transparent(0.3)} } + } + + .debug-pane .monaco-list-row .expression .value.changed { + background-color: ${debugViewValueChangedHighlightColor.transparent(0.3)}; + animation-name: debugViewletValueChanged; + animation-duration: 1s; + animation-fill-mode: forwards; + } + `); + + const contrastBorderColor = theme.getColor(contrastBorder); + + if (contrastBorderColor) { + collector.addRule(` + .debug-pane .line-number { + border: 1px solid ${contrastBorderColor}; + } + `); + } + + const tokenNameColor = theme.getColor(debugTokenExpressionName)!; + const tokenValueColor = theme.getColor(debugTokenExpressionValue)!; + const tokenStringColor = theme.getColor(debugTokenExpressionString)!; + const tokenBooleanColor = theme.getColor(debugTokenExpressionBoolean)!; + const tokenErrorColor = theme.getColor(debugTokenExpressionError)!; + const tokenNumberColor = theme.getColor(debugTokenExpressionNumber)!; + + collector.addRule(` + .monaco-workbench .monaco-list-row .expression .name { + color: ${tokenNameColor}; + } + + .monaco-workbench .monaco-list-row .expression .value, + .monaco-workbench .debug-hover-widget .value { + color: ${tokenValueColor}; + } + + .monaco-workbench .monaco-list-row .expression .value.string, + .monaco-workbench .debug-hover-widget .value.string { + color: ${tokenStringColor}; + } + + .monaco-workbench .monaco-list-row .expression .value.boolean, + .monaco-workbench .debug-hover-widget .value.boolean { + color: ${tokenBooleanColor}; + } + + .monaco-workbench .monaco-list-row .expression .error, + .monaco-workbench .debug-hover-widget .error, + .monaco-workbench .debug-pane .debug-variables .scope .error { + color: ${tokenErrorColor}; + } + + .monaco-workbench .monaco-list-row .expression .value.number, + .monaco-workbench .debug-hover-widget .value.number { + color: ${tokenNumberColor}; + } + `); + + const debugConsoleInputBorderColor = theme.getColor(inputBorder) || Color.fromHex('#80808060'); + const debugConsoleInfoForegroundColor = theme.getColor(debugConsoleInfoForeground)!; + const debugConsoleWarningForegroundColor = theme.getColor(debugConsoleWarningForeground)!; + const debugConsoleErrorForegroundColor = theme.getColor(debugConsoleErrorForeground)!; + const debugConsoleSourceForegroundColor = theme.getColor(debugConsoleSourceForeground)!; + const debugConsoleInputIconForegroundColor = theme.getColor(debugConsoleInputIconForeground)!; + + collector.addRule(` + .repl .repl-input-wrapper { + border-top: 1px solid ${debugConsoleInputBorderColor}; + } + + .monaco-workbench .repl .repl-tree .output .expression .value.info { + color: ${debugConsoleInfoForegroundColor}; + } + + .monaco-workbench .repl .repl-tree .output .expression .value.warn { + color: ${debugConsoleWarningForegroundColor}; + } + + .monaco-workbench .repl .repl-tree .output .expression .value.error { + color: ${debugConsoleErrorForegroundColor}; + } + + .monaco-workbench .repl .repl-tree .output .expression .source { + color: ${debugConsoleSourceForegroundColor}; + } + + .monaco-workbench .repl .repl-tree .monaco-tl-contents .arrow { + color: ${debugConsoleInputIconForegroundColor}; + } + `); + + if (!theme.defines(debugConsoleInputIconForeground)) { + collector.addRule(` + .monaco-workbench.vs .repl .repl-tree .monaco-tl-contents .arrow { + opacity: 0.25; + } + + .monaco-workbench.vs-dark .repl .repl-tree .monaco-tl-contents .arrow { + opacity: 0.4; + } + + .monaco-workbench.hc-black .repl .repl-tree .monaco-tl-contents .arrow { + opacity: 1; + } + `); + } + }); +} diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index e0ab9d8b528..e28eaf83505 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -21,7 +21,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IDebugConfigurationProvider, ICompound, IDebugConfiguration, IConfig, IGlobalConfig, IConfigurationManager, ILaunch, IDebugAdapterDescriptorFactory, IDebugAdapter, IDebugSession, IAdapterDescriptor, CONTEXT_DEBUG_CONFIGURATION_TYPE, IDebugAdapterFactory, IConfigPresentation } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugConfigurationProvider, ICompound, IDebugConfiguration, IConfig, IGlobalConfig, IConfigurationManager, ILaunch, IDebugAdapterDescriptorFactory, IDebugAdapter, IDebugSession, IAdapterDescriptor, CONTEXT_DEBUG_CONFIGURATION_TYPE, IDebugAdapterFactory, IConfigPresentation, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug'; import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -62,6 +62,7 @@ export class ConfigurationManager implements IConfigurationManager { private adapterDescriptorFactories: IDebugAdapterDescriptorFactory[]; private debugAdapterFactories = new Map(); private debugConfigurationTypeContext: IContextKey; + private debuggersAvailable: IContextKey; private readonly _onDidRegisterDebugger = new Emitter(); constructor( @@ -85,6 +86,7 @@ export class ConfigurationManager implements IConfigurationManager { const previousSelectedRoot = this.storageService.get(DEBUG_SELECTED_ROOT, StorageScope.WORKSPACE); const previousSelectedLaunch = this.launches.find(l => l.uri.toString() === previousSelectedRoot); this.debugConfigurationTypeContext = CONTEXT_DEBUG_CONFIGURATION_TYPE.bindTo(contextKeyService); + this.debuggersAvailable = CONTEXT_DEBUGGERS_AVAILABLE.bindTo(contextKeyService); if (previousSelectedLaunch && previousSelectedLaunch.getConfigurationNames().length) { this.selectConfiguration(previousSelectedLaunch, this.storageService.get(DEBUG_SELECTED_CONFIG_NAME_KEY, StorageScope.WORKSPACE)); } else if (this.launches.length > 0) { @@ -96,6 +98,9 @@ export class ConfigurationManager implements IConfigurationManager { registerDebugAdapterFactory(debugTypes: string[], debugAdapterLauncher: IDebugAdapterFactory): IDisposable { debugTypes.forEach(debugType => this.debugAdapterFactories.set(debugType, debugAdapterLauncher)); + this.debuggersAvailable.set(this.debugAdapterFactories.size > 0); + this._onDidRegisterDebugger.fire(); + return { dispose: () => { debugTypes.forEach(debugType => this.debugAdapterFactories.delete(debugType)); @@ -103,6 +108,10 @@ export class ConfigurationManager implements IConfigurationManager { }; } + hasDebuggers(): boolean { + return this.debugAdapterFactories.size > 0; + } + createDebugAdapter(session: IDebugSession): IDebugAdapter | undefined { let factory = this.debugAdapterFactories.get(session.configuration.type); if (factory) { @@ -312,6 +321,7 @@ export class ConfigurationManager implements IConfigurationManager { if (launch.workspace && provider) { picks.push(provider.provideDebugConfigurations!(launch.workspace.uri, token.token).then(configurations => configurations.map(config => ({ label: config.name, + description: launch.name, config, buttons: [{ iconClass: 'codicon-gear', @@ -416,7 +426,6 @@ export class ConfigurationManager implements IConfigurationManager { }); this.setCompoundSchemaValues(); - this._onDidRegisterDebugger.fire(); }); breakpointsExtPoint.setHandler((extensions, delta) => { diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index c7249d02612..f8350cb13d5 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -9,8 +9,7 @@ import { Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ServicesAccessor, registerEditorAction, EditorAction, IActionOptions } from 'vs/editor/browser/editorExtensions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, VIEWLET_ID, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, REPL_VIEW_ID, CONTEXT_STEP_INTO_TARGETS_SUPPORTED } from 'vs/workbench/contrib/debug/common/debug'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, REPL_VIEW_ID, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, WATCH_VIEW_ID, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { openBreakpointSource } from 'vs/workbench/contrib/debug/browser/breakpointsView'; @@ -28,7 +27,7 @@ class ToggleBreakpointAction extends EditorAction { id: TOGGLE_BREAKPOINT_ID, label: nls.localize('toggleBreakpointAction', "Debug: Toggle Breakpoint"), alias: 'Debug: Toggle Breakpoint', - precondition: undefined, + precondition: CONTEXT_DEBUGGERS_AVAILABLE, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyCode.F9, @@ -67,7 +66,7 @@ class ConditionalBreakpointAction extends EditorAction { id: TOGGLE_CONDITIONAL_BREAKPOINT_ID, label: nls.localize('conditionalBreakpointEditorAction', "Debug: Add Conditional Breakpoint..."), alias: 'Debug: Add Conditional Breakpoint...', - precondition: undefined + precondition: CONTEXT_DEBUGGERS_AVAILABLE }); } @@ -89,7 +88,7 @@ class LogPointAction extends EditorAction { id: ADD_LOG_POINT_ID, label: nls.localize('logPointEditorAction', "Debug: Add Logpoint..."), alias: 'Debug: Add Logpoint...', - precondition: undefined + precondition: CONTEXT_DEBUGGERS_AVAILABLE }); } @@ -144,7 +143,15 @@ export class RunToCursorAction extends EditorAction { const uri = editor.getModel().uri; const bpExists = !!(debugService.getModel().getBreakpoints({ column: position.column, lineNumber: position.lineNumber, uri }).length); if (!bpExists) { - const breakpoints = await debugService.addBreakpoints(uri, [{ lineNumber: position.lineNumber, column: position.column }], 'debugEditorActions.runToCursorAction'); + let column = 0; + const focusedStackFrame = debugService.getViewModel().focusedStackFrame; + if (focusedStackFrame && focusedStackFrame.source.uri.toString() === uri.toString() && focusedStackFrame.range.startLineNumber === position.lineNumber) { + // If the cursor is on a line different than the one the debugger is currently paused on, then send the breakpoint at column 0 on the line + // otherwise set it at the precise column #102199 + column = position.column; + } + + const breakpoints = await debugService.addBreakpoints(uri, [{ lineNumber: position.lineNumber, column }], 'debugEditorActions.runToCursorAction'); if (breakpoints && breakpoints.length) { breakpointToRemove = breakpoints[0]; } @@ -202,13 +209,13 @@ class SelectionToWatchExpressionsAction extends EditorAction { async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { const debugService = accessor.get(IDebugService); - const viewletService = accessor.get(IViewletService); + const viewsService = accessor.get(IViewsService); if (!editor.hasModel()) { return; } const text = editor.getModel().getValueInRange(editor.getSelection()); - await viewletService.openViewlet(VIEWLET_ID); + await viewsService.openView(WATCH_VIEW_ID); debugService.addWatchExpression(text); } } @@ -332,7 +339,7 @@ class GoToNextBreakpointAction extends GoToBreakpointAction { id: 'editor.debug.action.goToNextBreakpoint', label: nls.localize('goToNextBreakpoint', "Debug: Go To Next Breakpoint"), alias: 'Debug: Go To Next Breakpoint', - precondition: undefined + precondition: CONTEXT_DEBUGGERS_AVAILABLE }); } } @@ -343,18 +350,20 @@ class GoToPreviousBreakpointAction extends GoToBreakpointAction { id: 'editor.debug.action.goToPreviousBreakpoint', label: nls.localize('goToPreviousBreakpoint', "Debug: Go To Previous Breakpoint"), alias: 'Debug: Go To Previous Breakpoint', - precondition: undefined + precondition: CONTEXT_DEBUGGERS_AVAILABLE }); } } -registerEditorAction(ToggleBreakpointAction); -registerEditorAction(ConditionalBreakpointAction); -registerEditorAction(LogPointAction); -registerEditorAction(RunToCursorAction); -registerEditorAction(StepIntoTargetsAction); -registerEditorAction(SelectionToReplAction); -registerEditorAction(SelectionToWatchExpressionsAction); -registerEditorAction(ShowDebugHoverAction); -registerEditorAction(GoToNextBreakpointAction); -registerEditorAction(GoToPreviousBreakpointAction); +export function registerEditorActions(): void { + registerEditorAction(ToggleBreakpointAction); + registerEditorAction(ConditionalBreakpointAction); + registerEditorAction(LogPointAction); + registerEditorAction(RunToCursorAction); + registerEditorAction(StepIntoTargetsAction); + registerEditorAction(SelectionToReplAction); + registerEditorAction(SelectionToWatchExpressionsAction); + registerEditorAction(ShowDebugHoverAction); + registerEditorAction(GoToNextBreakpointAction); + registerEditorAction(GoToPreviousBreakpointAction); +} diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 711e00f179d..b8d3efa299b 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -14,7 +14,6 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { StandardTokenType } from 'vs/editor/common/modes'; import { DEFAULT_WORD_REGEXP } from 'vs/editor/common/model/wordHelper'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType, IPartialEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; -import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IDecorationOptions } from 'vs/editor/common/editorCommon'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { Range } from 'vs/editor/common/core/range'; @@ -22,7 +21,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IDebugEditorContribution, IDebugService, State, EDITOR_CONTRIBUTION_ID, IStackFrame, IDebugConfiguration, IExpression, IExceptionInfo, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugEditorContribution, IDebugService, State, IStackFrame, IDebugConfiguration, IExpression, IExceptionInfo, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; import { ExceptionWidget } from 'vs/workbench/contrib/debug/browser/exceptionWidget'; import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; import { Position } from 'vs/editor/common/core/position'; @@ -164,7 +163,7 @@ function getWordToLineNumbersMap(model: ITextModel | null): Map { this.onStateChange(); })); - this.toDispose.push(this.configurationManager.onDidSelectConfiguration(() => { - this.debugUx.set(!!(this.state !== State.Inactive || this.configurationManager.selectedConfiguration.name) ? 'default' : 'simple'); + this.toDispose.push(Event.any(this.configurationManager.onDidRegisterDebugger, this.configurationManager.onDidSelectConfiguration)(() => { + this.debugUx.set(!!(this.state !== State.Inactive || (this.configurationManager.selectedConfiguration.name && this.configurationManager.hasDebuggers())) ? 'default' : 'simple'); })); this.toDispose.push(this.model.onDidChangeCallStack(() => { const numberOfSessions = this.model.getSessions().filter(s => !s.parentSession).length; @@ -164,7 +169,10 @@ export class DebugService implements IDebugService { this.activity.dispose(); } if (numberOfSessions > 0) { - this.activity = this.activityService.showViewContainerActivity(VIEWLET_ID, { badge: new NumberBadge(numberOfSessions, n => n === 1 ? nls.localize('1activeSession', "1 active session") : nls.localize('nActiveSessions', "{0} active sessions", n)) }); + const viewContainer = this.viewDescriptorService.getViewContainerByViewId(CALLSTACK_VIEW_ID); + if (viewContainer) { + this.activity = this.activityService.showViewContainerActivity(viewContainer.id, { badge: new NumberBadge(numberOfSessions, n => n === 1 ? nls.localize('1activeSession', "1 active session") : nls.localize('nActiveSessions', "{0} active sessions", n)) }); + } } })); this.toDispose.push(this.model.onDidChangeBreakpoints(() => setBreakpointsExistContext())); @@ -235,7 +243,7 @@ export class DebugService implements IDebugService { this.debugState.set(getStateLabel(state)); this.inDebugMode.set(state !== State.Inactive); // Only show the simple ux if debug is not yet started and if no launch.json exists - this.debugUx.set(((state !== State.Inactive && state !== State.Initializing) || this.configurationManager.selectedConfiguration.name) ? 'default' : 'simple'); + this.debugUx.set(((state !== State.Inactive && state !== State.Initializing) || (this.configurationManager.hasDebuggers() && this.configurationManager.selectedConfiguration.name)) ? 'default' : 'simple'); }); this.previousState = state; this._onDidChangeState.fire(state); @@ -412,6 +420,12 @@ export class DebugService implements IDebugService { return false; } + const workspace = launch?.workspace || this.contextService.getWorkspace(); + const taskResult = await this.taskRunner.runTaskAndCheckErrors(workspace, resolvedConfig.preLaunchTask, (msg, actions) => this.showError(msg, actions)); + if (taskResult === TaskRunResult.Failure) { + return false; + } + const cfg = await this.configurationManager.resolveDebugConfigurationWithSubstitutedVariables(launch && launch.workspace ? launch.workspace.uri : undefined, type, resolvedConfig, initCancellationToken.token); if (!cfg) { if (launch && type && cfg === null && !initCancellationToken.token.isCancellationRequested) { // show launch.json only for "config" being "null". @@ -432,16 +446,22 @@ export class DebugService implements IDebugService { nls.localize('debugTypeMissing', "Missing property 'type' for the chosen launch configuration."); } - await this.showError(message); + const actionList: IAction[] = []; + + actionList.push(new Action( + 'installAdditionalDebuggers', + nls.localize('installAdditionalDebuggers', "Install {0} Extension", resolvedConfig.type), + undefined, + true, + async () => this.commandService.executeCommand('debug.installAdditionalDebuggers') + )); + + await this.showError(message, actionList); + return false; } - const workspace = launch?.workspace || this.contextService.getWorkspace(); - const taskResult = await this.taskRunner.runTaskAndCheckErrors(workspace, resolvedConfig.preLaunchTask, (msg, actions) => this.showError(msg, actions)); - if (taskResult === TaskRunResult.Success) { - return this.doCreateSession(sessionId, launch?.workspace, { resolved: resolvedConfig, unresolved: unresolvedConfig }, options); - } - return false; + return this.doCreateSession(sessionId, launch?.workspace, { resolved: resolvedConfig, unresolved: unresolvedConfig }, options); } catch (err) { if (err && err.message) { await this.showError(err.message); @@ -714,7 +734,7 @@ export class DebugService implements IDebugService { }); } - stopSession(session: IDebugSession | undefined): Promise { + async stopSession(session: IDebugSession | undefined): Promise { if (session) { return session.terminate(); } @@ -722,6 +742,8 @@ export class DebugService implements IDebugService { const sessions = this.model.getSessions(); if (sessions.length === 0) { this.taskRunner.cancel(); + // User might have cancelled starting of a debug session, and in some cases the quick pick is left open + await this.quickInputService.cancel(); this.endInitializingState(); this.cancelTokens(undefined); } @@ -990,11 +1012,8 @@ export function getStackFrameThreadAndSessionToFocus(model: IDebugModel, stackFr } } - if (!stackFrame) { - if (thread) { - const callStack = thread.getCallStack(); - stackFrame = first(callStack, sf => !!(sf && sf.source && sf.source.available && sf.source.presentationHint !== 'deemphasize'), undefined); - } + if (!stackFrame && thread) { + stackFrame = thread.getTopStackFrame(); } return { session, thread, stackFrame }; diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index cd67a54853f..22373d30863 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -440,6 +440,10 @@ export class DebugSession implements IDebugSession { return distinct(positions, p => `${p.lineNumber}:${p.column}`); } + getDebugProtocolBreakpoint(breakpointId: string): DebugProtocol.Breakpoint | undefined { + return this.model.getDebugProtocolBreakpoint(breakpointId, this.getId()); + } + customRequest(request: string, args: any): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", request)); @@ -448,13 +452,13 @@ export class DebugSession implements IDebugSession { return this.raw.custom(request, args); } - stackTrace(threadId: number, startFrame: number, levels: number): Promise { + stackTrace(threadId: number, startFrame: number, levels: number, token: CancellationToken): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'stackTrace')); } - const token = this.getNewCancellationToken(threadId); - return this.raw.stackTrace({ threadId, startFrame, levels }, token); + const sessionToken = this.getNewCancellationToken(threadId, token); + return this.raw.stackTrace({ threadId, startFrame, levels }, sessionToken); } async exceptionInfo(threadId: number): Promise { @@ -628,17 +632,18 @@ export class DebugSession implements IDebugSession { } } - async completions(frameId: number | undefined, text: string, position: Position, overwriteBefore: number, token: CancellationToken): Promise { + async completions(frameId: number | undefined, threadId: number, text: string, position: Position, overwriteBefore: number, token: CancellationToken): Promise { if (!this.raw) { return Promise.reject(new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'completions'))); } + const sessionCancelationToken = this.getNewCancellationToken(threadId, token); return this.raw.completions({ frameId, text, column: position.column, line: position.lineNumber, - }, token); + }, sessionCancelationToken); } async stepInTargets(frameId: number): Promise<{ id: number, label: string }[]> { @@ -1053,8 +1058,8 @@ export class DebugSession implements IDebugSession { } } - private getNewCancellationToken(threadId: number): CancellationToken { - const tokenSource = new CancellationTokenSource(); + private getNewCancellationToken(threadId: number, token?: CancellationToken): CancellationToken { + const tokenSource = new CancellationTokenSource(token); const tokens = this.cancellationMap.get(threadId) || []; tokens.push(tokenSource); this.cancellationMap.set(threadId, tokens); diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 54c7593b579..7f60e726eef 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -21,7 +21,6 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { registerThemingParticipant, IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { registerColor, contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { localize } from 'vs/nls'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -58,7 +57,6 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { @IThemeService themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IMenuService menuService: IMenuService, - @IContextMenuService contextMenuService: IContextMenuService, @IContextKeyService contextKeyService: IContextKeyService ) { super(themeService); diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index bbe7ac03099..d32774028e5 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -31,6 +31,9 @@ import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views import { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView'; import { ToggleViewAction } from 'vs/workbench/browser/actions/layoutActions'; import { RunOnceScheduler } from 'vs/base/common/async'; +import { ShowViewletAction } from 'vs/workbench/browser/viewlet'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; export class DebugViewPaneContainer extends ViewPaneContainer { @@ -248,3 +251,18 @@ export class OpenDebugConsoleAction extends ToggleViewAction { super(id, label, REPL_VIEW_ID, viewsService, viewDescriptorService, contextKeyService, layoutService, 'codicon-debug-console'); } } + +export class OpenDebugViewletAction extends ShowViewletAction { + public static readonly ID = VIEWLET_ID; + public static readonly LABEL = nls.localize('toggleDebugViewlet', "Show Run and Debug"); + + constructor( + id: string, + label: string, + @IViewletService viewletService: IViewletService, + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService + ) { + super(id, label, VIEWLET_ID, viewletService, editorGroupService, layoutService); + } +} diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index b7e09375af9..b6c1ee1f468 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -211,7 +211,7 @@ margin-left: 0.8em; } -.debug-pane .debug-call-stack .load-more { +.debug-pane .debug-call-stack .load-all { text-align: center; } diff --git a/src/vs/workbench/contrib/debug/browser/media/repl.css b/src/vs/workbench/contrib/debug/browser/media/repl.css index 1970134e9c3..2eaa06164d0 100644 --- a/src/vs/workbench/contrib/debug/browser/media/repl.css +++ b/src/vs/workbench/contrib/debug/browser/media/repl.css @@ -35,7 +35,7 @@ } .monaco-workbench .repl .repl-tree .output.expression.value-and-source .value { - flex: 1; + margin-right: 4px; } .monaco-workbench .repl .repl-tree .monaco-tl-contents .arrow { @@ -44,14 +44,14 @@ } .monaco-workbench .repl .repl-tree .output.expression.value-and-source .source { - margin-left: 4px; + margin-left: auto; margin-right: 8px; cursor: pointer; text-decoration: underline; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - max-width: 150px; + text-align: right; } .monaco-workbench .repl .repl-tree .output.expression > .value, @@ -87,3 +87,34 @@ .monaco-workbench .repl .repl-tree .output.expression .code-bold { font-weight: bold; } .monaco-workbench .repl .repl-tree .output.expression .code-italic { font-style: italic; } .monaco-workbench .repl .repl-tree .output.expression .code-underline { text-decoration: underline; } + +.monaco-action-bar .action-item.repl-panel-filter-container { + cursor: default; + display: flex; +} + +.monaco-action-bar .panel-action-tree-filter{ + display: flex; + align-items: center; + flex: 1; +} + +.monaco-action-bar .panel-action-tree-filter .monaco-inputbox { + height: 24px; + font-size: 12px; + flex: 1; +} + +.pane-header .monaco-action-bar .panel-action-tree-filter .monaco-inputbox { + height: 20px; + line-height: 18px; +} + +.monaco-workbench.vs .monaco-action-bar .panel-action-tree-filter .monaco-inputbox { + height: 25px; +} + +.panel > .title .monaco-action-bar .action-item.repl-panel-filter-container { + min-width: 200px; + margin-right: 10px; +} diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index a400dde3bd5..720ed115418 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -281,7 +281,7 @@ export class RawDebugSession implements IDisposable { if (this.capabilities.supportsTerminateRequest) { if (!this.terminated) { this.terminated = true; - return this.send('terminate', { restart }, undefined, 1000); + return this.send('terminate', { restart }, undefined, 2000); } return this.disconnect(restart); } @@ -499,7 +499,9 @@ export class RawDebugSession implements IDisposable { this.inShutdown = true; if (this.debugAdapter) { try { - await this.send('disconnect', { restart }, undefined, 1000); + await this.send('disconnect', { restart }, undefined, 2000); + } catch (e) { + // Catch the potential 'disconnect' error - no need to show it to the user since the adapter is shutting down } finally { this.stopAdapter(error); } diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 6afa8ef0e4f..7aa395b1c9b 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -59,12 +59,13 @@ import { ReplGroup } from 'vs/workbench/contrib/debug/common/replModel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { EDITOR_FONT_DEFAULTS, EditorOption } from 'vs/editor/common/config/editorOptions'; import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; +import { ReplFilter, ReplFilterState, ReplFilterActionViewItem } from 'vs/workbench/contrib/debug/browser/replFilter'; const $ = dom.$; const HISTORY_STORAGE_KEY = 'debug.repl.history'; const DECORATION_KEY = 'replinputdecoration'; - +const FILTER_ACTION_ID = `workbench.actions.treeView.repl.filter`; function revealLastElement(tree: WorkbenchAsyncDataTree) { tree.scrollTop = tree.scrollHeight - tree.renderHeight; @@ -93,6 +94,9 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { private styleElement: HTMLStyleElement | undefined; private completionItemProvider: IDisposable | undefined; private modelChangeListener: IDisposable = Disposable.None; + private filter: ReplFilter; + private filterState: ReplFilterState; + private filterActionViewItem: ReplFilterActionViewItem | undefined; constructor( options: IViewPaneOptions, @@ -116,6 +120,9 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 50); + this.filter = new ReplFilter(); + this.filterState = new ReplFilterState(); + codeEditorService.registerDecorationType(DECORATION_KEY, {}); this.registerListeners(); } @@ -141,7 +148,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { const text = model.getValue(); const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; const frameId = focusedStackFrame ? focusedStackFrame.frameId : undefined; - const response = await session.completions(frameId, text, position, overwriteBefore, token); + const response = await session.completions(frameId, focusedStackFrame?.thread.threadId || 0, text, position, overwriteBefore, token); const suggestions: CompletionItem[] = []; const computeRange = (length: number) => Range.fromPositions(position.delta(0, -length), position); @@ -237,6 +244,12 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this._register(this.editorService.onDidActiveEditorChange(() => { this.setMode(); })); + + this._register(this.filterState.onDidChange(() => { + this.filter.filterQuery = this.filterState.filterText; + this.tree.refilter(); + revealLastElement(this.tree); + })); } get isReadonly(): boolean { @@ -257,8 +270,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.navigateHistory(false); } - focusRepl(): void { - this.tree.domFocus(); + focusFilter(): void { + this.filterActionViewItem?.focus(); } private setMode(): void { @@ -437,6 +450,9 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { getActionViewItem(action: IAction): IActionViewItem | undefined { if (action.id === SelectReplAction.ID) { return this.instantiationService.createInstance(SelectReplActionViewItem, this.selectReplAction); + } else if (action.id === FILTER_ACTION_ID) { + this.filterActionViewItem = this.instantiationService.createInstance(ReplFilterActionViewItem, action, localize('workbench.debug.filter.placeholder', "Filter (e.g. text, !exclude)"), this.filterState); + return this.filterActionViewItem; } return super.getActionViewItem(action); @@ -444,6 +460,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { getActions(): IAction[] { const result: IAction[] = []; + result.push(new Action(FILTER_ACTION_ID)); if (this.debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl() && !sessionsToIgnore.has(s)).length > 1) { result.push(this.selectReplAction); } @@ -532,6 +549,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { // https://github.com/microsoft/TypeScript/issues/32526 new ReplDataSource() as IAsyncDataSource, { + filter: this.filter, accessibilityProvider: new ReplAccessibilityProvider(), identityProvider: { getId: (element: IReplElement) => element.getId() }, mouseSupport: false, @@ -740,7 +758,7 @@ class FilterReplAction extends EditorAction { run(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise { SuggestController.get(editor).acceptSelectedSuggestion(false, true); const repl = getReplView(accessor.get(IViewsService)); - repl?.focusRepl(); + repl?.focusFilter(); } } diff --git a/src/vs/workbench/contrib/debug/browser/replFilter.ts b/src/vs/workbench/contrib/debug/browser/replFilter.ts new file mode 100644 index 00000000000..50b2b862673 --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/replFilter.ts @@ -0,0 +1,184 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { matchesFuzzy } from 'vs/base/common/filters'; +import { splitGlobAware } from 'vs/base/common/glob'; +import * as strings from 'vs/base/common/strings'; +import { ITreeFilter, TreeVisibility, TreeFilterResult } from 'vs/base/browser/ui/tree/tree'; +import { IReplElement } from 'vs/workbench/contrib/debug/common/debug'; +import * as DOM from 'vs/base/browser/dom'; +import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { Delayer } from 'vs/base/common/async'; +import { IAction } from 'vs/base/common/actions'; +import { HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import { Event, Emitter } from 'vs/base/common/event'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { ContextScopedHistoryInputBox } from 'vs/platform/browser/contextScopedHistoryWidget'; +import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; + + +type ParsedQuery = { + type: 'include' | 'exclude', + query: string, +}; + +export class ReplFilter implements ITreeFilter { + + static matchQuery = matchesFuzzy; + + private _parsedQueries: ParsedQuery[] = []; + set filterQuery(query: string) { + this._parsedQueries = []; + query = query.trim(); + + if (query && query !== '') { + const filters = splitGlobAware(query, ',').map(s => s.trim()).filter(s => !!s.length); + for (const f of filters) { + if (strings.startsWith(f, '!')) { + this._parsedQueries.push({ type: 'exclude', query: f.slice(1) }); + } else { + this._parsedQueries.push({ type: 'include', query: f }); + } + } + } + } + + filter(element: IReplElement, parentVisibility: TreeVisibility): TreeFilterResult { + if (this._parsedQueries.length === 0) { + return parentVisibility; + } + + let includeQueryPresent = false; + let includeQueryMatched = false; + + const text = element.toString(); + + for (let { type, query } of this._parsedQueries) { + if (type === 'exclude' && ReplFilter.matchQuery(query, text)) { + // If exclude query matches, ignore all other queries and hide + return false; + } else if (type === 'include') { + includeQueryPresent = true; + if (ReplFilter.matchQuery(query, text)) { + includeQueryMatched = true; + } + } + } + + return includeQueryPresent ? includeQueryMatched : parentVisibility; + } +} + +export class ReplFilterState { + + private readonly _onDidChange: Emitter = new Emitter(); + get onDidChange(): Event { + return this._onDidChange.event; + } + + private _filterText = ''; + + get filterText(): string { + return this._filterText; + } + + set filterText(filterText: string) { + if (this._filterText !== filterText) { + this._filterText = filterText; + this._onDidChange.fire(); + } + } +} + +export class ReplFilterActionViewItem extends BaseActionViewItem { + + private delayedFilterUpdate: Delayer; + private container!: HTMLElement; + private filterInputBox!: HistoryInputBox; + + constructor( + action: IAction, + private placeholder: string, + private filters: ReplFilterState, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IThemeService private readonly themeService: IThemeService, + @IContextViewService private readonly contextViewService: IContextViewService) { + super(null, action); + this.delayedFilterUpdate = new Delayer(200); + this._register(toDisposable(() => this.delayedFilterUpdate.cancel())); + } + + render(container: HTMLElement): void { + this.container = container; + DOM.addClass(this.container, 'repl-panel-filter-container'); + + this.element = DOM.append(this.container, DOM.$('')); + this.element.className = this.class; + this.createInput(this.element); + this.updateClass(); + } + + focus(): void { + this.filterInputBox.focus(); + } + + private clearFilterText(): void { + this.filterInputBox.value = ''; + } + + private createInput(container: HTMLElement): void { + this.filterInputBox = this._register(this.instantiationService.createInstance(ContextScopedHistoryInputBox, container, this.contextViewService, { + placeholder: this.placeholder, + history: [] + })); + this._register(attachInputBoxStyler(this.filterInputBox, this.themeService)); + this.filterInputBox.value = this.filters.filterText; + + this._register(this.filterInputBox.onDidChange(() => this.delayedFilterUpdate.trigger(() => this.onDidInputChange(this.filterInputBox!)))); + this._register(this.filters.onDidChange(() => { + this.filterInputBox.value = this.filters.filterText; + })); + this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, DOM.EventType.KEY_DOWN, (e: any) => this.onInputKeyDown(e))); + this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_DOWN, this.handleKeyboardEvent)); + this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_UP, this.handleKeyboardEvent)); + this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, DOM.EventType.CLICK, (e) => { + e.stopPropagation(); + e.preventDefault(); + })); + } + + private onDidInputChange(inputbox: HistoryInputBox) { + inputbox.addToHistory(); + this.filters.filterText = inputbox.value; + } + + // Action toolbar is swallowing some keys for action items which should not be for an input box + private handleKeyboardEvent(event: StandardKeyboardEvent) { + if (event.equals(KeyCode.Space) + || event.equals(KeyCode.LeftArrow) + || event.equals(KeyCode.RightArrow) + || event.equals(KeyCode.Escape) + ) { + event.stopPropagation(); + } + } + + private onInputKeyDown(event: StandardKeyboardEvent) { + if (event.equals(KeyCode.Escape)) { + this.clearFilterText(); + event.stopPropagation(); + event.preventDefault(); + } + } + + protected get class(): string { + return 'panel-action-tree-filter'; + } +} diff --git a/src/vs/workbench/contrib/debug/browser/replViewer.ts b/src/vs/workbench/contrib/debug/browser/replViewer.ts index ea99871de6d..99b071635d4 100644 --- a/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -181,7 +181,7 @@ export class ReplSimpleElementsRenderer implements ITreeRenderer element.sourceData; } diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 16b982c463a..a001777f000 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -8,7 +8,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import * as dom from 'vs/base/browser/dom'; import { CollapseAction } from 'vs/workbench/browser/viewlet'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { IDebugService, IExpression, IScope, CONTEXT_VARIABLES_FOCUSED, IStackFrame } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IExpression, IScope, CONTEXT_VARIABLES_FOCUSED, IStackFrame, CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT } from 'vs/workbench/contrib/debug/common/debug'; import { Variable, Scope, ErrorScope, StackFrame } from 'vs/workbench/contrib/debug/common/debugModel'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -27,13 +27,15 @@ import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { dispose } from 'vs/base/common/lifecycle'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { withUndefinedAsNull } from 'vs/base/common/types'; +import { IMenuService, IMenu, MenuId } from 'vs/platform/actions/common/actions'; +import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; const $ = dom.$; let forgetScopes = true; @@ -47,6 +49,8 @@ export class VariablesView extends ViewPane { private tree!: WorkbenchAsyncDataTree; private savedViewState = new Map(); private autoExpandedScopes = new Set(); + private menu: IMenu; + private debugProtocolVariableMenuContext: IContextKey; constructor( options: IViewletViewOptions, @@ -61,9 +65,14 @@ export class VariablesView extends ViewPane { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, + @IMenuService menuService: IMenuService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + this.menu = menuService.createMenu(MenuId.DebugVariablesContext, contextKeyService); + this._register(this.menu); + this.debugProtocolVariableMenuContext = CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT.bindTo(contextKeyService); + // Use scheduler to prevent unnecessary flashing this.onFocusStackFrameScheduler = new RunOnceScheduler(async () => { const stackFrame = this.debugService.getViewModel().focusedStackFrame; @@ -186,20 +195,18 @@ export class VariablesView extends ViewPane { const actions: IAction[] = []; const session = this.debugService.getViewModel().focusedSession; if (session && session.capabilities.supportsSetVariable) { - actions.push(new Action('workbench.setValue', nls.localize('setValue', "Set Value"), undefined, true, () => { + actions.push(new Action('workbench.setValue', nls.localize('setValue', "Set Value"), undefined, true, async () => { this.debugService.getViewModel().setSelectedExpression(variable); - return Promise.resolve(); })); } actions.push(this.instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, variable, 'variables')); if (variable.evaluateName) { - actions.push(new Action('debug.copyEvaluatePath', nls.localize('copyAsExpression', "Copy as Expression"), undefined, true, () => { - return this.clipboardService.writeText(variable.evaluateName!); + actions.push(new Action('debug.copyEvaluatePath', nls.localize('copyAsExpression', "Copy as Expression"), undefined, true, async () => { + await this.clipboardService.writeText(variable.evaluateName!); })); actions.push(new Separator()); - actions.push(new Action('debug.addToWatchExpressions', nls.localize('addToWatchExpressions', "Add to Watch"), undefined, true, () => { + actions.push(new Action('debug.addToWatchExpressions', nls.localize('addToWatchExpressions', "Add to Watch"), undefined, true, async () => { this.debugService.addWatchExpression(variable.evaluateName); - return Promise.resolve(undefined); })); } if (session && session.capabilities.supportsDataBreakpoints) { @@ -207,17 +214,24 @@ export class VariablesView extends ViewPane { const dataid = response?.dataId; if (response && dataid) { actions.push(new Separator()); - actions.push(new Action('debug.breakWhenValueChanges', nls.localize('breakWhenValueChanges', "Break When Value Changes"), undefined, true, () => { - return this.debugService.addDataBreakpoint(response.description, dataid, !!response.canPersist, response.accessTypes); + actions.push(new Action('debug.breakWhenValueChanges', nls.localize('breakWhenValueChanges', "Break When Value Changes"), undefined, true, async () => { + await this.debugService.addDataBreakpoint(response.description, dataid, !!response.canPersist, response.accessTypes); })); } } + const context = { + container: (variable.parent as (Variable | Scope)).toDebugProtocolObject(), + variable: variable.toDebugProtocolObject() + }; + const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: context, shouldForwardArgs: false }, actions, this.contextMenuService); + this.debugProtocolVariableMenuContext.set(variable.variableMenuContext || ''); + this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, getActions: () => actions, getActionsContext: () => variable, - onHide: () => dispose(actions) + onHide: () => dispose(actionsDisposable) }); } } diff --git a/src/vs/workbench/contrib/debug/browser/welcomeView.ts b/src/vs/workbench/contrib/debug/browser/welcomeView.ts index d9238fcd9e7..b0bad1df2f2 100644 --- a/src/vs/workbench/contrib/debug/browser/welcomeView.ts +++ b/src/vs/workbench/contrib/debug/browser/welcomeView.ts @@ -8,10 +8,10 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; 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 { IContextKeyService, RawContextKey, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { localize } from 'vs/nls'; import { StartAction, ConfigureAction, SelectAndStartAction } from 'vs/workbench/contrib/debug/browser/debugActions'; -import { IDebugService } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -109,30 +109,32 @@ const viewsRegistry = Registry.as(Extensions.ViewsRegistry); viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'openAFileWhichCanBeDebugged', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, "[Open a file](command:{0}) which can be debugged or run.", isMacintosh ? OpenFileFolderAction.ID : OpenFileAction.ID), - when: CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR.toNegated() + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR.toNegated()) }); let debugKeybindingLabel = ''; viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'runAndDebugAction', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, "[Run and Debug{0}](command:{1})", debugKeybindingLabel, StartAction.ID), - preconditions: [CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR] + preconditions: [CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR], + when: CONTEXT_DEBUGGERS_AVAILABLE }); viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'detectThenRunAndDebug', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, "[Show](command:{0}) all automatic debug configurations.", SelectAndStartAction.ID), - priority: ViewContentPriority.Lowest + priority: ViewContentPriority.Lowest, + when: CONTEXT_DEBUGGERS_AVAILABLE }); viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'customizeRunAndDebug', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, "To customize Run and Debug [create a launch.json file](command:{0}).", ConfigureAction.ID), - when: WorkbenchStateContext.notEqualsTo('empty') + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, WorkbenchStateContext.notEqualsTo('empty')) }); viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'customizeRunAndDebugOpenFolder', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, "To customize Run and Debug, [open a folder](command:{0}) and create a launch.json file.", isMacintosh ? OpenFileFolderAction.ID : OpenFolderAction.ID), - when: WorkbenchStateContext.isEqualTo('empty') + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, WorkbenchStateContext.isEqualTo('empty')) }); diff --git a/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts b/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts index 93a76d52c1e..8b5ab068ee3 100644 --- a/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts +++ b/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts @@ -6,6 +6,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IDebugAdapter } from 'vs/workbench/contrib/debug/common/debug'; import { timeout } from 'vs/base/common/async'; +import { localize } from 'vs/nls'; /** * Abstract implementation of the low level API for a debug adapter. @@ -88,7 +89,7 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { request_seq: request.seq, success: false, command, - message: `timeout after ${timeout} ms` + message: localize('timeout', "Timeout after {0} ms for '{1}'", timeout, command) }; clb(err); } diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index d74d8cce52f..8092d6da302 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -59,6 +59,8 @@ export const CONTEXT_RESTART_FRAME_SUPPORTED = new RawContextKey('resta export const CONTEXT_JUMP_TO_CURSOR_SUPPORTED = new RawContextKey('jumpToCursorSupported', false); export const CONTEXT_STEP_INTO_TARGETS_SUPPORTED = new RawContextKey('stepIntoTargetsSupported', false); export const CONTEXT_BREAKPOINTS_EXIST = new RawContextKey('breakpointsExist', false); +export const CONTEXT_DEBUGGERS_AVAILABLE = new RawContextKey('debuggersAvailable', false); +export const CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT = new RawContextKey('debugProtocolVariableMenuContext', undefined); export const EDITOR_CONTRIBUTION_ID = 'editor.contrib.debug'; export const BREAKPOINT_EDITOR_CONTRIBUTION_ID = 'editor.contrib.breakpoint'; @@ -224,8 +226,9 @@ export interface IDebugSession extends ITreeElement { sendDataBreakpoints(dbps: IDataBreakpoint[]): Promise; sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise; breakpointsLocations(uri: uri, lineNumber: number): Promise; + getDebugProtocolBreakpoint(breakpointId: string): DebugProtocol.Breakpoint | undefined; - stackTrace(threadId: number, startFrame: number, levels: number): Promise; + stackTrace(threadId: number, startFrame: number, levels: number, token: CancellationToken): Promise; exceptionInfo(threadId: number): Promise; scopes(frameId: number, threadId: number): Promise; variables(variablesReference: number, threadId: number | undefined, filter: 'indexed' | 'named' | undefined, start: number | undefined, count: number | undefined): Promise; @@ -244,7 +247,7 @@ export interface IDebugSession extends ITreeElement { terminateThreads(threadIds: number[]): Promise; stepInTargets(frameId: number): Promise<{ id: number, label: string }[]>; - completions(frameId: number | undefined, text: string, position: Position, overwriteBefore: number, token: CancellationToken): Promise; + completions(frameId: number | undefined, threadId: number, text: string, position: Position, overwriteBefore: number, token: CancellationToken): Promise; setVariable(variablesReference: number | undefined, name: string, value: string): Promise; loadSource(resource: uri): Promise; getLoadedSources(): Promise; @@ -288,6 +291,12 @@ export interface IThread extends ITreeElement { */ getCallStack(): ReadonlyArray; + + /** + * Gets the top stack frame that is not hidden if the callstack has already been received from the debug adapter + */ + getTopStackFrame(): IStackFrame | undefined; + /** * Invalidates the callstack cache */ @@ -566,6 +575,11 @@ export interface IDebugAdapterServer { readonly host?: string; } +export interface IDebugAdapterNamedPipeServer { + readonly type: 'pipeServer'; + readonly path: string; +} + export interface IDebugAdapterInlineImpl extends IDisposable { readonly onDidSendMessage: Event; handleMessage(message: DebugProtocol.Message): void; @@ -576,7 +590,7 @@ export interface IDebugAdapterImpl { readonly implementation: IDebugAdapterInlineImpl; } -export type IAdapterDescriptor = IDebugAdapterExecutable | IDebugAdapterServer | IDebugAdapterImpl; +export type IAdapterDescriptor = IDebugAdapterExecutable | IDebugAdapterServer | IDebugAdapterNamedPipeServer | IDebugAdapterImpl; export interface IPlatformSpecificAdapterContribution { program?: string; @@ -651,6 +665,8 @@ export interface IConfigurationManager { getLaunches(): ReadonlyArray; + hasDebuggers(): boolean; + getLaunch(workspaceUri: uri | undefined): ILaunch | undefined; getAllConfigurations(): { launch: ILaunch, name: string, presentation?: IConfigPresentation }[]; diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 1a8a5879ca6..b47d112dca0 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -10,7 +10,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { generateUuid } from 'vs/base/common/uuid'; import { RunOnceScheduler } from 'vs/base/common/async'; import { isString, isUndefinedOrNull } from 'vs/base/common/types'; -import { distinct, lastIndex } from 'vs/base/common/arrays'; +import { distinct, lastIndex, first } from 'vs/base/common/arrays'; import { Range, IRange } from 'vs/editor/common/core/range'; import { ITreeElement, IExpression, IExpressionContainer, IDebugSession, IStackFrame, IExceptionBreakpoint, IBreakpoint, IFunctionBreakpoint, IDebugModel, @@ -22,6 +22,11 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile import { ITextEditorPane } from 'vs/workbench/common/editor'; import { mixin } from 'vs/base/common/objects'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; + +interface IDebugProtocolVariableWithContext extends DebugProtocol.Variable { + __vscodeVariableMenuContext?: string; +} export class ExpressionContainer implements IExpressionContainer { @@ -85,7 +90,7 @@ export class ExpressionContainer implements IExpressionContainer { for (let i = 0; i < numberOfChunks; i++) { const start = (this.startOfVariables || 0) + i * chunkSize; const count = Math.min(chunkSize, this.indexedVariables - i * chunkSize); - children.push(new Variable(this.session, this.threadId, this, this.reference, `[${start}..${start + count - 1}]`, '', '', undefined, count, { kind: 'virtual' }, undefined, true, start)); + children.push(new Variable(this.session, this.threadId, this, this.reference, `[${start}..${start + count - 1}]`, '', '', undefined, count, { kind: 'virtual' }, undefined, undefined, true, start)); } return children; @@ -116,14 +121,14 @@ export class ExpressionContainer implements IExpressionContainer { try { const response = await this.session!.variables(this.reference || 0, this.threadId, filter, start, count); return response && response.body && response.body.variables - ? distinct(response.body.variables.filter(v => !!v), v => v.name).map(v => { + ? distinct(response.body.variables.filter(v => !!v), v => v.name).map((v: IDebugProtocolVariableWithContext) => { if (isString(v.value) && isString(v.name) && typeof v.variablesReference === 'number') { - return new Variable(this.session, this.threadId, this, v.variablesReference, v.name, v.evaluateName, v.value, v.namedVariables, v.indexedVariables, v.presentationHint, v.type); + return new Variable(this.session, this.threadId, this, v.variablesReference, v.name, v.evaluateName, v.value, v.namedVariables, v.indexedVariables, v.presentationHint, v.type, v.__vscodeVariableMenuContext); } - return new Variable(this.session, this.threadId, this, 0, '', undefined, nls.localize('invalidVariableAttributes', "Invalid variable attributes"), 0, 0, { kind: 'virtual' }, undefined, false); + return new Variable(this.session, this.threadId, this, 0, '', undefined, nls.localize('invalidVariableAttributes', "Invalid variable attributes"), 0, 0, { kind: 'virtual' }, undefined, undefined, false); }) : []; } catch (e) { - return [new Variable(this.session, this.threadId, this, 0, '', undefined, e.message, 0, 0, { kind: 'virtual' }, undefined, false)]; + return [new Variable(this.session, this.threadId, this, 0, '', undefined, e.message, 0, 0, { kind: 'virtual' }, undefined, undefined, false)]; } } @@ -217,6 +222,7 @@ export class Variable extends ExpressionContainer implements IExpression { indexedVariables: number | undefined, public presentationHint: DebugProtocol.VariablePresentationHint | undefined, public type: string | undefined = undefined, + public variableMenuContext: string | undefined = undefined, public available = true, startOfVariables = 0 ) { @@ -246,6 +252,14 @@ export class Variable extends ExpressionContainer implements IExpression { toString(): string { return `${this.name}: ${this.value}`; } + + toDebugProtocolObject(): DebugProtocol.Variable { + return { + name: this.name, + variablesReference: this.reference || 0, + value: this.value + }; + } } export class Scope extends ExpressionContainer implements IScope { @@ -266,6 +280,14 @@ export class Scope extends ExpressionContainer implements IScope { toString(): string { return this.name; } + + toDebugProtocolObject(): DebugProtocol.Scope { + return { + name: this.name, + variablesReference: this.reference || 0, + expensive: this.expensive + }; + } } export class ErrorScope extends Scope { @@ -366,6 +388,7 @@ export class StackFrame implements IStackFrame { export class Thread implements IThread { private callStack: IStackFrame[]; private staleCallStack: IStackFrame[]; + private callStackCancellationTokens: CancellationTokenSource[] = []; public stoppedDetails: IRawStoppedDetails | undefined; public stopped: boolean; @@ -384,6 +407,8 @@ export class Thread implements IThread { this.staleCallStack = this.callStack; } this.callStack = []; + this.callStackCancellationTokens.forEach(c => c.dispose(true)); + this.callStackCancellationTokens = []; } getCallStack(): IStackFrame[] { @@ -394,6 +419,10 @@ export class Thread implements IThread { return this.staleCallStack; } + getTopStackFrame(): IStackFrame | undefined { + return first(this.getCallStack(), sf => !!(sf && sf.source && sf.source.available && sf.source.presentationHint !== 'deemphasize'), undefined); + } + get stateLabel(): string { if (this.stoppedDetails) { return this.stoppedDetails.description || @@ -424,8 +453,10 @@ export class Thread implements IThread { private async getCallStackImpl(startFrame: number, levels: number): Promise { try { - const response = await this.session.stackTrace(this.threadId, startFrame, levels); - if (!response || !response.body) { + const tokenSource = new CancellationTokenSource(); + this.callStackCancellationTokens.push(tokenSource); + const response = await this.session.stackTrace(this.threadId, startFrame, levels, tokenSource.token); + if (!response || !response.body || tokenSource.token.isCancellationRequested) { return []; } @@ -587,6 +618,26 @@ export abstract class BaseBreakpoint extends Enablement implements IBaseBreakpoi return data ? data.id : undefined; } + getDebugProtocolBreakpoint(sessionId: string): DebugProtocol.Breakpoint | undefined { + const data = this.sessionData.get(sessionId); + if (data) { + const bp: DebugProtocol.Breakpoint = { + id: data.id, + verified: data.verified, + message: data.message, + source: data.source, + line: data.line, + column: data.column, + endLine: data.endLine, + endColumn: data.endColumn, + instructionReference: data.instructionReference, + offset: data.offset + }; + return bp; + } + return undefined; + } + toJSON(): any { const result = Object.create(null); result.enabled = this.enabled; @@ -1084,6 +1135,14 @@ export class DebugModel implements IDebugModel { }); } + getDebugProtocolBreakpoint(breakpointId: string, sessionId: string): DebugProtocol.Breakpoint | undefined { + const bp = this.breakpoints.find(bp => bp.getId() === breakpointId); + if (bp) { + return bp.getDebugProtocolBreakpoint(sessionId); + } + return undefined; + } + private sortAndDeDup(): void { this.breakpoints = this.breakpoints.sort((first, second) => { if (first.uri.toString() !== second.uri.toString()) { diff --git a/src/vs/workbench/contrib/debug/node/debugAdapter.ts b/src/vs/workbench/contrib/debug/node/debugAdapter.ts index 70ddc23e1b5..dc6d941a068 100644 --- a/src/vs/workbench/contrib/debug/node/debugAdapter.ts +++ b/src/vs/workbench/contrib/debug/node/debugAdapter.ts @@ -14,7 +14,7 @@ import * as objects from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; import { ExtensionsChannelId } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IOutputService } from 'vs/workbench/contrib/output/common/output'; -import { IDebugAdapterExecutable, IDebuggerContribution, IPlatformSpecificAdapterContribution, IDebugAdapterServer } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugAdapterExecutable, IDebuggerContribution, IPlatformSpecificAdapterContribution, IDebugAdapterServer, IDebugAdapterNamedPipeServer } from 'vs/workbench/contrib/debug/common/debug'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { AbstractDebugAdapter } from '../common/abstractDebugAdapter'; @@ -91,25 +91,22 @@ export abstract class StreamDebugAdapter extends AbstractDebugAdapter { } } -/** - * An implementation that connects to a debug adapter via a socket. -*/ -export class SocketDebugAdapter extends StreamDebugAdapter { +export abstract class NetworkDebugAdapter extends StreamDebugAdapter { - private socket?: net.Socket; + protected socket?: net.Socket; - constructor(private adapterServer: IDebugAdapterServer) { - super(); - } + protected abstract createConnection(connectionListener: () => void): net.Socket; startSession(): Promise { return new Promise((resolve, reject) => { let connected = false; - this.socket = net.createConnection(this.adapterServer.port, this.adapterServer.host || '127.0.0.1', () => { + + this.socket = this.createConnection(() => { this.connect(this.socket!, this.socket!); resolve(); connected = true; }); + this.socket.on('close', () => { if (connected) { this._onError.fire(new Error('connection closed')); @@ -117,6 +114,7 @@ export class SocketDebugAdapter extends StreamDebugAdapter { reject(new Error('connection closed')); } }); + this.socket.on('error', error => { if (connected) { this._onError.fire(error); @@ -136,6 +134,34 @@ export class SocketDebugAdapter extends StreamDebugAdapter { } } +/** + * An implementation that connects to a debug adapter via a socket. +*/ +export class SocketDebugAdapter extends NetworkDebugAdapter { + + constructor(private adapterServer: IDebugAdapterServer) { + super(); + } + + protected createConnection(connectionListener: () => void): net.Socket { + return net.createConnection(this.adapterServer.port, this.adapterServer.host || '127.0.0.1', connectionListener); + } +} + +/** + * An implementation that connects to a debug adapter via a NamedPipe (on Windows)/UNIX Domain Socket (on non-Windows). + */ +export class NamedPipeDebugAdapter extends NetworkDebugAdapter { + + constructor(private adapterServer: IDebugAdapterNamedPipeServer) { + super(); + } + + protected createConnection(connectionListener: () => void): net.Socket { + return net.createConnection(this.adapterServer.path, connectionListener); + } +} + /** * An implementation that launches the debug adapter as a separate process and communicates via stdin/stdout. */ diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index 3c5fcd77c9a..0c27cb6d55c 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -177,7 +177,7 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? command += `cd ${quote(cwd)} ; `; } if (env) { - command += 'env'; + command += '/usr/bin/env'; for (let key in env) { const value = env[key]; if (value === null) { diff --git a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts index 7dff5eea6e9..596687ed5a0 100644 --- a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts @@ -42,8 +42,8 @@ function createTwoStackFrames(session: DebugSession): { firstStackFrame: StackFr sourceReference: 11, }, 'aDebugSessionId'); - firstStackFrame = new StackFrame(thread, 1, firstSource, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1); - secondStackFrame = new StackFrame(thread, 1, secondSource, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1); + firstStackFrame = new StackFrame(thread, 0, firstSource, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 0); + secondStackFrame = new StackFrame(thread, 1, secondSource, 'app2.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1); return { firstStackFrame, secondStackFrame }; } @@ -297,7 +297,7 @@ suite('Debug - CallStack', () => { const session = createMockSession(model); model.addSession(session); const { firstStackFrame, secondStackFrame } = createTwoStackFrames(session); - let decorations = createDecorationsForStackFrame(firstStackFrame, firstStackFrame.range); + let decorations = createDecorationsForStackFrame(firstStackFrame, firstStackFrame.range, true); assert.equal(decorations.length, 2); assert.deepEqual(decorations[0].range, new Range(1, 2, 1, 1)); assert.equal(decorations[0].options.glyphMarginClassName, 'codicon-debug-stackframe'); @@ -305,7 +305,7 @@ suite('Debug - CallStack', () => { assert.equal(decorations[1].options.className, 'debug-top-stack-frame-line'); assert.equal(decorations[1].options.isWholeLine, true); - decorations = createDecorationsForStackFrame(secondStackFrame, firstStackFrame.range); + decorations = createDecorationsForStackFrame(secondStackFrame, firstStackFrame.range, true); assert.equal(decorations.length, 2); assert.deepEqual(decorations[0].range, new Range(1, 2, 1, 1)); assert.equal(decorations[0].options.glyphMarginClassName, 'codicon-debug-stackframe-focused'); @@ -313,7 +313,7 @@ suite('Debug - CallStack', () => { assert.equal(decorations[1].options.className, 'debug-focused-stack-frame-line'); assert.equal(decorations[1].options.isWholeLine, true); - decorations = createDecorationsForStackFrame(firstStackFrame, new Range(1, 5, 1, 6)); + decorations = createDecorationsForStackFrame(firstStackFrame, new Range(1, 5, 1, 6), true); assert.equal(decorations.length, 3); assert.deepEqual(decorations[0].range, new Range(1, 2, 1, 1)); assert.equal(decorations[0].options.glyphMarginClassName, 'codicon-debug-stackframe'); diff --git a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts index a8c0c42173e..787328d4e5d 100644 --- a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts @@ -12,6 +12,8 @@ import { SimpleReplElement, RawObjectReplElement, ReplEvaluationInput, ReplModel import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession'; import { timeout } from 'vs/base/common/async'; import { createMockSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; +import { ReplFilter } from 'vs/workbench/contrib/debug/browser/replFilter'; +import { TreeVisibility } from 'vs/base/browser/ui/tree/tree'; suite('Debug - REPL', () => { let model: DebugModel; @@ -189,4 +191,62 @@ suite('Debug - REPL', () => { assert.equal(repl.getReplElements().length, 3); assert.equal((repl.getReplElements()[2]).value, 'second global line'); }); + + test('repl filter', async () => { + const session = createMockSession(model); + const repl = new ReplModel(); + const replFilter = new ReplFilter(); + + const getFilteredElements = () => { + const elements = repl.getReplElements(); + return elements.filter(e => { + const filterResult = replFilter.filter(e, TreeVisibility.Visible); + return filterResult === true || filterResult === TreeVisibility.Visible; + }); + }; + + repl.appendToRepl(session, 'first line\n', severity.Info); + repl.appendToRepl(session, 'second line\n', severity.Info); + repl.appendToRepl(session, 'third line\n', severity.Info); + repl.appendToRepl(session, 'fourth line\n', severity.Info); + + replFilter.filterQuery = 'first'; + let r1 = getFilteredElements(); + assert.equal(r1.length, 1); + assert.equal(r1[0].value, 'first line\n'); + + replFilter.filterQuery = '!first'; + let r2 = getFilteredElements(); + assert.equal(r1.length, 1); + assert.equal(r2[0].value, 'second line\n'); + assert.equal(r2[1].value, 'third line\n'); + assert.equal(r2[2].value, 'fourth line\n'); + + replFilter.filterQuery = 'first, line'; + let r3 = getFilteredElements(); + assert.equal(r3.length, 4); + assert.equal(r3[0].value, 'first line\n'); + assert.equal(r3[1].value, 'second line\n'); + assert.equal(r3[2].value, 'third line\n'); + assert.equal(r3[3].value, 'fourth line\n'); + + replFilter.filterQuery = 'line, !second'; + let r4 = getFilteredElements(); + assert.equal(r4.length, 3); + assert.equal(r4[0].value, 'first line\n'); + assert.equal(r4[1].value, 'third line\n'); + assert.equal(r4[2].value, 'fourth line\n'); + + replFilter.filterQuery = '!second, line'; + let r4_same = getFilteredElements(); + assert.equal(r4.length, r4_same.length); + + replFilter.filterQuery = '!line'; + let r5 = getFilteredElements(); + assert.equal(r5.length, 0); + + replFilter.filterQuery = 'smth'; + let r6 = getFilteredElements(); + assert.equal(r6.length, 0); + }); }); diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 7e9b4377b82..9f4002da8b9 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -14,6 +14,7 @@ import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstract import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { ExceptionBreakpoint, Expression, DataBreakpoint, FunctionBreakpoint, Breakpoint, DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class MockDebugService implements IDebugService { @@ -266,7 +267,7 @@ export class MockSession implements IDebugSession { return Promise.resolve([]); } - completions(frameId: number, text: string, position: Position, overwriteBefore: number): Promise { + completions(frameId: number, threadId: number, text: string, position: Position, overwriteBefore: number): Promise { throw new Error('not implemented'); } @@ -292,10 +293,13 @@ export class MockSession implements IDebugSession { sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise { throw new Error('Method not implemented.'); } + getDebugProtocolBreakpoint(breakpointId: string): DebugProtocol.Breakpoint | undefined { + throw new Error('Method not implemented.'); + } customRequest(request: string, args: any): Promise { throw new Error('Method not implemented.'); } - stackTrace(threadId: number, startFrame: number, levels: number): Promise { + stackTrace(threadId: number, startFrame: number, levels: number, token: CancellationToken): Promise { throw new Error('Method not implemented.'); } exceptionInfo(threadId: number): Promise { diff --git a/src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts b/src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts index 9b504a180a9..abe90db10c1 100644 --- a/src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts +++ b/src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts @@ -88,7 +88,6 @@ suite('Debug - ANSI Handling', () => { return child; } else { assert.fail('Unexpected assertion error'); - return null!; } } diff --git a/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts b/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts new file mode 100644 index 00000000000..470565e2d7a --- /dev/null +++ b/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as crypto from 'crypto'; +import * as net from 'net'; +import * as platform from 'vs/base/common/platform'; +import { tmpdir } from 'os'; +import { join } from 'vs/base/common/path'; +import { SocketDebugAdapter, NamedPipeDebugAdapter, StreamDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; + +function rndPort(): number { + const min = 8000; + const max = 9000; + return Math.floor(Math.random() * (max - min) + min); +} + +function sendInitializeRequest(debugAdapter: StreamDebugAdapter): Promise { + return new Promise((resolve, reject) => { + debugAdapter.sendRequest('initialize', { adapterID: 'test' }, (result) => { + resolve(result); + }); + }); +} + +function serverConnection(socket: net.Socket) { + socket.on('data', (data: Buffer) => { + const str = data.toString().split('\r\n')[2]; + const request = JSON.parse(str); + const response: any = { + seq: request.seq, + request_seq: request.seq, + type: 'response', + command: request.command + }; + if (request.arguments.adapterID === 'test') { + response.success = true; + } else { + response.success = false; + response.message = 'failed'; + } + + const responsePayload = JSON.stringify(response); + socket.write(`Content-Length: ${responsePayload.length}\r\n\r\n${responsePayload}`); + }); +} + +suite('Debug - StreamDebugAdapter', () => { + const port = rndPort(); + const pipeName = crypto.randomBytes(10).toString('hex'); + const pipePath = platform.isWindows ? join('\\\\.\\pipe\\', pipeName) : join(tmpdir(), pipeName); + + const testCases: { testName: string, debugAdapter: StreamDebugAdapter, connectionDetail: string | number }[] = [ + { + testName: 'NamedPipeDebugAdapter', + debugAdapter: new NamedPipeDebugAdapter({ + type: 'pipeServer', + path: pipePath + }), + connectionDetail: pipePath + }, + { + testName: 'SocketDebugAdapter', + debugAdapter: new SocketDebugAdapter({ + type: 'server', + port + }), + connectionDetail: port + } + ]; + + for (const testCase of testCases) { + test(`StreamDebugAdapter (${testCase.testName}) can initialize a connection`, async () => { + const server = net.createServer(serverConnection).listen(testCase.connectionDetail); + const debugAdapter = testCase.debugAdapter; + try { + await debugAdapter.startSession(); + const response: DebugProtocol.Response = await sendInitializeRequest(debugAdapter); + assert.strictEqual(response.command, 'initialize'); + assert.strictEqual(response.request_seq, 1); + assert.strictEqual(response.success, true, response.message); + } finally { + await debugAdapter.stopSession(); + server.close(); + debugAdapter.dispose(); + } + }); + } +}); diff --git a/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts b/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts index 048c43ef01a..c56afa2d157 100644 --- a/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts +++ b/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts @@ -56,14 +56,12 @@ suite('Emmet', () => { const model = editor.getModel(); if (!model) { assert.fail('Editor model not found'); - return; } model.setMode(languageIdentifier); let langOutput = EmmetEditorAction.getLanguage(languageIdentifierResolver, editor, new MockGrammarContributions(scopeName)); if (!langOutput) { assert.fail('langOutput not found'); - return; } assert.equal(langOutput.language, expectedLanguage); diff --git a/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts index 8215282b35b..ca5d30091db 100644 --- a/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IExtensionTipsService, IExtensionManagementService, ILocalExtension, IConfigBasedExtensionTip } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionTipsService, IConfigBasedExtensionTip, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { localize } from 'vs/nls'; @@ -14,13 +14,17 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IStorageService } from 'vs/platform/storage/common/storage'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { IWorkspaceContextService, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; -import { distinct } from 'vs/base/common/arrays'; +import { Emitter } from 'vs/base/common/event'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; export class ConfigBasedRecommendations extends ExtensionRecommendations { private importantTips: IConfigBasedExtensionTip[] = []; private otherTips: IConfigBasedExtensionTip[] = []; + private _onDidChangeRecommendations = this._register(new Emitter()); + readonly onDidChangeRecommendations = this._onDidChangeRecommendations.event; + private _otherRecommendations: ExtensionRecommendation[] = []; get otherRecommendations(): ReadonlyArray { return this._otherRecommendations; } @@ -32,8 +36,9 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations { constructor( isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, @IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @INotificationService notificationService: INotificationService, @@ -41,13 +46,12 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations { @IStorageService storageService: IStorageService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, extensionsWorkbenchService, extensionManagementService, storageKeysSyncRegistryService); } protected async doActivate(): Promise { await this.fetch(); this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e))); - this.promptWorkspaceRecommendations(); } private async fetch(): Promise { @@ -70,54 +74,13 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations { this._importantRecommendations = this.importantTips.map(tip => this.toExtensionRecommendation(tip)); } - private async promptWorkspaceRecommendations(): Promise { - if (this.hasToIgnoreRecommendationNotifications()) { - return; - } - - if (this.importantTips.length === 0) { - return; - } - - const local = await this.extensionManagementService.getInstalled(); - const { uninstalled } = this.groupByInstalled(distinct(this.importantTips.map(({ extensionId }) => extensionId)), local); - if (uninstalled.length === 0) { - return; - } - - const importantExtensions = this.filterIgnoredOrNotAllowed(uninstalled); - if (importantExtensions.length === 0) { - return; - } - - for (const extension of importantExtensions) { - const tip = this.importantTips.filter(tip => tip.extensionId === extension)[0]; - const message = tip.isExtensionPack ? localize('extensionPackRecommended', "The '{0}' extension pack is recommended for this workspace.", tip.extensionName) - : localize('extensionRecommended', "The '{0}' extension is recommended for this workspace.", tip.extensionName); - this.promptImportantExtensionsInstallNotification([extension], message); - } - } - - private groupByInstalled(recommendationsToSuggest: string[], local: ILocalExtension[]): { installed: string[], uninstalled: string[] } { - const installed: string[] = [], uninstalled: string[] = []; - const installedExtensionsIds = local.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set()); - recommendationsToSuggest.forEach(id => { - if (installedExtensionsIds.has(id.toLowerCase())) { - installed.push(id); - } else { - uninstalled.push(id); - } - }); - return { installed, uninstalled }; - } - private async onWorkspaceFoldersChanged(event: IWorkspaceFoldersChangeEvent): Promise { if (event.added.length) { const oldImportantRecommended = this.importantTips; await this.fetch(); // Suggest only if at least one of the newly added recommendations was not suggested before if (this.importantTips.some(current => oldImportantRecommended.every(old => current.extensionId !== old.extensionId))) { - return this.promptWorkspaceRecommendations(); + this._onDidChangeRecommendations.fire(); } } } diff --git a/src/vs/workbench/contrib/extensions/browser/dynamicWorkspaceRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/dynamicWorkspaceRecommendations.ts index 39694d6c0a7..17c8c513833 100644 --- a/src/vs/workbench/contrib/extensions/browser/dynamicWorkspaceRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/dynamicWorkspaceRecommendations.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IFileService } from 'vs/platform/files/common/files'; @@ -18,6 +18,7 @@ import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionMa import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; type DynamicWorkspaceRecommendationsClassification = { count: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -44,9 +45,11 @@ export class DynamicWorkspaceRecommendations extends ExtensionRecommendations { @INotificationService notificationService: INotificationService, @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, extensionsWorkbenchService, extensionManagementService, storageKeysSyncRegistryService); } protected async doActivate(): Promise { diff --git a/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts index 60a16d6fc88..a9911fa979a 100644 --- a/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts @@ -8,7 +8,6 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { timeout } from 'vs/base/common/async'; import { localize } from 'vs/nls'; -import { IStringDictionary } from 'vs/base/common/collections'; import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { basename } from 'vs/base/common/path'; @@ -17,6 +16,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IStorageService } from 'vs/platform/storage/common/storage'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; type ExeExtensionRecommendationsClassification = { extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' }; @@ -25,12 +25,11 @@ type ExeExtensionRecommendationsClassification = { export class ExeBasedRecommendations extends ExtensionRecommendations { + private _otherTips: IExecutableBasedExtensionTip[] = []; + private _importantTips: IExecutableBasedExtensionTip[] = []; - private readonly _otherRecommendations: ExtensionRecommendation[] = []; - get otherRecommendations(): ReadonlyArray { return this._otherRecommendations; } - - private readonly _importantRecommendations: ExtensionRecommendation[] = []; - get importantRecommendations(): ReadonlyArray { return this._importantRecommendations; } + get otherRecommendations(): ReadonlyArray { return this._otherTips.map(tip => this.toExtensionRecommendation(tip)); } + get importantRecommendations(): ReadonlyArray { return this._importantTips.map(tip => this.toExtensionRecommendation(tip)); } get recommendations(): ReadonlyArray { return [...this.importantRecommendations, ...this.otherRecommendations]; } @@ -39,7 +38,8 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { constructor( isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, @IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @optional(ITASExperimentService) tasExperimentService: ITASExperimentService, @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @@ -48,7 +48,7 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { @IStorageService storageService: IStorageService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, extensionsWorkbenchService, extensionManagementService, storageKeysSyncRegistryService); this.tasExperimentService = tasExperimentService; /* @@ -58,27 +58,35 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { timeout(3000).then(() => this.fetchAndPromptImportantExeBasedRecommendations()); } + getRecommendations(exe: string): { important: ExtensionRecommendation[], others: ExtensionRecommendation[] } { + const important = this._importantTips + .filter(tip => tip.exeName.toLowerCase() === exe.toLowerCase()) + .map(tip => this.toExtensionRecommendation(tip)); + + const others = this._otherTips + .filter(tip => tip.exeName.toLowerCase() === exe.toLowerCase()) + .map(tip => this.toExtensionRecommendation(tip)); + + return { important, others }; + } + protected async doActivate(): Promise { - const otherExectuableBasedTips = await this.extensionTipsService.getOtherExecutableBasedTips(); - otherExectuableBasedTips.forEach(tip => this._otherRecommendations.push(this.toExtensionRecommendation(tip))); + this._otherTips = await this.extensionTipsService.getOtherExecutableBasedTips(); await this.fetchImportantExeBasedRecommendations(); } - private _importantExeBasedRecommendations: Promise> | undefined; - private async fetchImportantExeBasedRecommendations(): Promise> { + private _importantExeBasedRecommendations: Promise> | undefined; + private async fetchImportantExeBasedRecommendations(): Promise> { if (!this._importantExeBasedRecommendations) { this._importantExeBasedRecommendations = this.doFetchImportantExeBasedRecommendations(); } return this._importantExeBasedRecommendations; } - private async doFetchImportantExeBasedRecommendations(): Promise> { - const importantExeBasedRecommendations: IStringDictionary = {}; - const importantExectuableBasedTips = await this.extensionTipsService.getImportantExecutableBasedTips(); - importantExectuableBasedTips.forEach(tip => { - this._importantRecommendations.push(this.toExtensionRecommendation(tip)); - importantExeBasedRecommendations[tip.extensionId.toLowerCase()] = tip; - }); + private async doFetchImportantExeBasedRecommendations(): Promise> { + const importantExeBasedRecommendations = new Map(); + this._importantTips = await this.extensionTipsService.getImportantExecutableBasedTips(); + this._importantTips.forEach(tip => importantExeBasedRecommendations.set(tip.extensionId.toLowerCase(), tip)); return importantExeBasedRecommendations; } @@ -86,22 +94,26 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { const importantExeBasedRecommendations = await this.fetchImportantExeBasedRecommendations(); const local = await this.extensionManagementService.getInstalled(); - const { installed, uninstalled } = this.groupByInstalled(Object.keys(importantExeBasedRecommendations), local); + const { installed, uninstalled } = this.groupByInstalled([...importantExeBasedRecommendations.keys()], local); /* Log installed and uninstalled exe based recommendations */ for (const extensionId of installed) { - const tip = importantExeBasedRecommendations[extensionId]; - this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:alreadyInstalled', { extensionId, exeName: basename(tip.windowsPath!) }); + const tip = importantExeBasedRecommendations.get(extensionId); + if (tip) { + this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:alreadyInstalled', { extensionId, exeName: basename(tip.windowsPath!) }); + } } for (const extensionId of uninstalled) { - const tip = importantExeBasedRecommendations[extensionId]; - this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:notInstalled', { extensionId, exeName: basename(tip.windowsPath!) }); + const tip = importantExeBasedRecommendations.get(extensionId); + if (tip) { + this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:notInstalled', { extensionId, exeName: basename(tip.windowsPath!) }); + } } this.promptImportantExeBasedRecommendations(uninstalled, importantExeBasedRecommendations); } - private async promptImportantExeBasedRecommendations(recommendations: string[], importantExeBasedRecommendations: IStringDictionary): Promise { + private async promptImportantExeBasedRecommendations(recommendations: string[], importantExeBasedRecommendations: Map): Promise { if (this.hasToIgnoreRecommendationNotifications()) { return; } @@ -112,13 +124,15 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { const recommendationsByExe = new Map(); for (const extensionId of recommendations) { - const tip = importantExeBasedRecommendations[extensionId]; - let tips = recommendationsByExe.get(tip.exeFriendlyName); - if (!tips) { - tips = []; - recommendationsByExe.set(tip.exeFriendlyName, tips); + const tip = importantExeBasedRecommendations.get(extensionId); + if (tip) { + let tips = recommendationsByExe.get(tip.exeFriendlyName); + if (!tips) { + tips = []; + recommendationsByExe.set(tip.exeFriendlyName, tips); + } + tips.push(tip); } - tips.push(tip); } for (const [, tips] of recommendationsByExe) { @@ -127,22 +141,8 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { await this.tasExperimentService.getTreatment('wslpopupaa'); } - if (tips.length === 1) { - const tip = tips[0]; - const message = tip.isExtensionPack ? localize('extensionPackRecommended', "The '{0}' extension pack is recommended as you have {1} installed on your system.", tip.extensionName, tip.exeFriendlyName || basename(tip.windowsPath!)) - : localize('exeRecommended', "The '{0}' extension is recommended as you have {1} installed on your system.", tip.extensionName, tip.exeFriendlyName || basename(tip.windowsPath!)); - this.promptImportantExtensionsInstallNotification(extensionIds, message); - } - - else if (tips.length === 2) { - const message = localize('two extensions recommended', "The '{0}' and '{1}' extensions are recommended as you have {2} installed on your system.", tips[0].extensionName, tips[1].extensionName, tips[0].exeFriendlyName || basename(tips[0].windowsPath!)); - this.promptImportantExtensionsInstallNotification(extensionIds, message); - } - - else if (tips.length > 2) { - const message = localize('more than two extensions recommended', "The '{0}', '{1}' and other extensions are recommended as you have {2} installed on your system.", tips[0].extensionName, tips[1].extensionName, tips[0].exeFriendlyName || basename(tips[0].windowsPath!)); - this.promptImportantExtensionsInstallNotification(extensionIds, message); - } + const message = localize('exeRecommended', "You have {0} installed on your system. Do you want to install the recommended extensions for it?", tips[0].exeFriendlyName); + this.promptImportantExtensionsInstallNotification(extensionIds, message, `@exe:"${tips[0].exeName}"`); } } diff --git a/src/vs/workbench/contrib/extensions/browser/experimentalRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/experimentalRecommendations.ts index 1e30aee3b93..c24644779fd 100644 --- a/src/vs/workbench/contrib/extensions/browser/experimentalRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/experimentalRecommendations.ts @@ -13,6 +13,8 @@ import { IExperimentService, ExperimentActionType, ExperimentState } from 'vs/wo import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; export class ExperimentalRecommendations extends ExtensionRecommendations { @@ -27,9 +29,11 @@ export class ExperimentalRecommendations extends ExtensionRecommendations { @INotificationService notificationService: INotificationService, @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, extensionsWorkbenchService, extensionManagementService, storageKeysSyncRegistryService); } /** diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 51ca34594bc..b0198b8aab4 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -15,7 +15,7 @@ import { isPromiseCanceledError } from 'vs/base/common/errors'; import { dispose, toDisposable, Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { domEvent } from 'vs/base/browser/event'; import { append, $, addClass, removeClass, finalHandler, join, toggleClass, hide, show, addDisposableListener, EventType } from 'vs/base/browser/dom'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -25,7 +25,7 @@ import { ResolvedKeybinding, KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, IExtension, ExtensionContainers } from 'vs/workbench/contrib/extensions/common/extensions'; import { RatingsWidget, InstallCountWidget, RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; -import { EditorOptions } from 'vs/workbench/common/editor'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { CombinedInstallAction, UpdateAction, ExtensionEditorDropDownAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, ExtensionToolTipAction, SystemDisabledWarningAction, LocalInstallAction, SyncIgnoredIconAction, SetProductIconThemeAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -164,7 +164,7 @@ interface IExtensionEditorTemplate { header: HTMLElement; } -export class ExtensionEditor extends BaseEditor { +export class ExtensionEditor extends EditorPane { static readonly ID: string = 'workbench.editor.extension'; @@ -313,8 +313,8 @@ export class ExtensionEditor extends BaseEditor { return disposables; } - async setInput(input: ExtensionsInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - await super.setInput(input, options, token); + async setInput(input: ExtensionsInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); if (this.template) { await this.updateTemplate(input, this.template, !!options?.preserveFocus); } @@ -429,7 +429,7 @@ export class ExtensionEditor extends BaseEditor { } this.setSubText(extension, reloadAction, template); - template.content.innerHTML = ''; // Clear content before setting navbar actions. + template.content.innerText = ''; // Clear content before setting navbar actions. template.navbar.clear(); @@ -560,7 +560,7 @@ export class ExtensionEditor extends BaseEditor { } this.contentDisposables.clear(); - template.content.innerHTML = ''; + template.content.innerText = ''; this.activeElement = null; if (id) { this.open(id, extension, template) @@ -904,6 +904,7 @@ export class ExtensionEditor extends BaseEditor { this.renderViews(content, manifest, layout), this.renderLocalizations(content, manifest, layout), this.renderCustomEditors(content, manifest, layout), + this.renderAuthentication(content, manifest, layout), ]; scrollableContent.scanDomNode(); @@ -1151,6 +1152,32 @@ export class ExtensionEditor extends BaseEditor { return true; } + private renderAuthentication(container: HTMLElement, manifest: IExtensionManifest, onDetailsToggle: Function): boolean { + const authentication = manifest.contributes?.authentication || []; + if (!authentication.length) { + return false; + } + + const details = $('details', { open: true, ontoggle: onDetailsToggle }, + $('summary', { tabindex: '0' }, localize('authentication', "Authentication ({0})", authentication.length)), + $('table', undefined, + $('tr', undefined, + $('th', undefined, localize('authentication.label', "Label")), + $('th', undefined, localize('authentication.id', "Id")) + ), + ...authentication.map(action => + $('tr', undefined, + $('td', undefined, action.label), + $('td', undefined, action.id) + ) + ) + ) + ); + + append(container, details); + return true; + } + private renderColorThemes(container: HTMLElement, manifest: IExtensionManifest, onDetailsToggle: Function): boolean { const contrib = manifest.contributes?.themes || []; if (!contrib.length) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts index 4347c796632..caf2db0f27a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts @@ -8,13 +8,15 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { localize } from 'vs/nls'; -import { InstallRecommendedExtensionAction, ShowRecommendedExtensionAction, ShowRecommendedExtensionsAction, InstallRecommendedExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { ExtensionRecommendationSource, IExtensionRecommendationReson } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { IExtensionsConfiguration, ConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtensionsConfiguration, ConfigurationKey, IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { IAction } from 'vs/base/common/actions'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { CancellationToken } from 'vs/base/common/cancellation'; type ExtensionRecommendationsNotificationClassification = { userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; @@ -42,6 +44,8 @@ export abstract class ExtensionRecommendations extends Disposable { @INotificationService protected readonly notificationService: INotificationService, @ITelemetryService protected readonly telemetryService: ITelemetryService, @IStorageService protected readonly storageService: IStorageService, + @IExtensionsWorkbenchService protected readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionManagementService protected readonly extensionManagementService: IExtensionManagementService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { super(); @@ -57,47 +61,60 @@ export abstract class ExtensionRecommendations extends Disposable { return this._activationPromise; } - private runAction(action: IAction) { + private async runAction(action: IAction): Promise { try { - action.run(); + await action.run(); } finally { action.dispose(); } } - protected promptImportantExtensionsInstallNotification(extensionIds: string[], message: string): void { + private async getInstallableExtensions(extensionIds: string[]): Promise { + const extensions: IExtension[] = []; + if (extensionIds.length) { + const pager = await this.extensionsWorkbenchService.queryGallery({ names: extensionIds, pageSize: extensionIds.length, source: 'install-recommendations' }, CancellationToken.None); + for (const extension of pager.firstPage) { + if (extension.gallery && (await this.extensionManagementService.canInstall(extension.gallery))) { + extensions.push(extension); + } + } + } + return extensions; + } + + protected async promptImportantExtensionsInstallNotification(extensionIds: string[], message: string, searchValue: string): Promise { + const extensions = await this.getInstallableExtensions(extensionIds); + if (!extensions.length) { + return; + } + this.notificationService.prompt(Severity.Info, message, [{ - label: extensionIds.length === 1 ? localize('install', 'Install') : localize('installAll', "Install All"), + label: localize('install', 'Install'), run: async () => { - for (const extensionId of extensionIds) { - this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId }); - } - if (extensionIds.length === 1) { - this.runAction(this.instantiationService.createInstance(InstallRecommendedExtensionAction, extensionIds[0])); - } else { - this.runAction(this.instantiationService.createInstance(InstallRecommendedExtensionsAction, InstallRecommendedExtensionsAction.ID, InstallRecommendedExtensionsAction.LABEL, extensionIds, 'install-recommendations')); - } + this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue)); + await Promise.all(extensions.map(async extension => { + this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId: extension.identifier.id }); + this.extensionsWorkbenchService.open(extension, { pinned: true }); + await this.extensionManagementService.installFromGallery(extension.gallery!); + })); } }, { - label: extensionIds.length === 1 ? localize('moreInformation', "More Information") : localize('showRecommendations', "Show Recommendations"), - run: () => { - for (const extensionId of extensionIds) { - this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId }); - } - if (extensionIds.length === 1) { - this.runAction(this.instantiationService.createInstance(ShowRecommendedExtensionAction, extensionIds[0])); - } else { - this.runAction(this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, ShowRecommendedExtensionsAction.LABEL)); + label: localize('show recommendations', "Show Recommendations"), + run: async () => { + for (const extension of extensions) { + this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId: extension.identifier.id }); + this.extensionsWorkbenchService.open(extension, { pinned: true }); } + this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue)); } }, { label: choiceNever, isSecondary: true, run: () => { - for (const extensionId of extensionIds) { - this.addToImportantRecommendationsIgnore(extensionId); - this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId }); + for (const extension of extensions) { + this.addToImportantRecommendationsIgnore(extension.identifier.id); + this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: extension.identifier.id }); } this.notificationService.prompt( Severity.Info, @@ -115,8 +132,8 @@ export abstract class ExtensionRecommendations extends Disposable { { sticky: true, onCancel: () => { - for (const extensionId of extensionIds) { - this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId }); + for (const extension of extensions) { + this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: extension.identifier.id }); } } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index 559f5346458..3f4cab7e871 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -5,10 +5,10 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IExtensionManagementService, IExtensionGalleryService, InstallOperation, DidInstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IExtensionRecommendationsService, ExtensionRecommendationReason, RecommendationChangeNotification, IExtensionRecommendation, ExtensionRecommendationSource } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IExtensionRecommendationsService, ExtensionRecommendationReason, RecommendationChangeNotification, IExtensionRecommendation, ExtensionRecommendationSource, EnablementState, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ShowRecommendationsOnlyOnDemandKey } from 'vs/workbench/contrib/extensions/common/extensions'; +import { ConfigurationKey, IExtension, IExtensionsConfiguration, IExtensionsWorkbenchService, ShowRecommendationsOnlyOnDemandKey } from 'vs/workbench/contrib/extensions/common/extensions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { distinct, shuffle } from 'vs/base/common/arrays'; @@ -25,13 +25,24 @@ import { KeymapRecommendations } from 'vs/workbench/contrib/extensions/browser/k import { ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { ConfigBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/configBasedRecommendations'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import Severity from 'vs/base/common/severity'; +import { localize } from 'vs/nls'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { CancellationToken } from 'vs/base/common/cancellation'; type IgnoreRecommendationClassification = { recommendationReason: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' }; }; +type ExtensionWorkspaceRecommendationsNotificationClassification = { + userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; +}; + const ignoredRecommendationsStorageKey = 'extensionsAssistant/ignored_recommendations'; +const ignoreWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore'; export class ExtensionRecommendationsService extends Disposable implements IExtensionRecommendationsService { @@ -49,15 +60,15 @@ export class ExtensionRecommendationsService extends Disposable implements IExte // Ignored Recommendations private globallyIgnoredRecommendations: string[] = []; - public loadWorkspaceConfigPromise: Promise; + public readonly activationPromise: Promise; private sessionSeed: number; private readonly _onRecommendationChange = this._register(new Emitter()); onRecommendationChange: Event = this._onRecommendationChange.event; constructor( - @IInstantiationService instantiationService: IInstantiationService, - @ILifecycleService lifecycleService: ILifecycleService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IStorageService private readonly storageService: IStorageService, @@ -65,6 +76,9 @@ export class ExtensionRecommendationsService extends Disposable implements IExte @ITelemetryService private readonly telemetryService: ITelemetryService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, + @INotificationService private readonly notificationService: INotificationService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, ) { super(); @@ -81,7 +95,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte if (!this.isEnabled()) { this.sessionSeed = 0; - this.loadWorkspaceConfigPromise = Promise.resolve(); + this.activationPromise = Promise.resolve(); return; } @@ -89,17 +103,33 @@ export class ExtensionRecommendationsService extends Disposable implements IExte this.globallyIgnoredRecommendations = this.getCachedIgnoredRecommendations(); // Activation - this.loadWorkspaceConfigPromise = this.workspaceRecommendations.activate().then(() => this.fileBasedRecommendations.activate()); - this.experimentalRecommendations.activate(); - this.keymapRecommendations.activate(); - if (!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)) { - lifecycleService.when(LifecyclePhase.Eventually).then(() => this.activateProactiveRecommendations()); - } + this.activationPromise = this.activate(); this._register(this.extensionManagementService.onDidInstallExtension(e => this.onDidInstallExtension(e))); this._register(this.storageService.onDidChangeStorage(e => this.onDidStorageChange(e))); } + private async activate(): Promise { + await this.lifecycleService.when(LifecyclePhase.Restored); + + // activate all recommendations + await Promise.all([ + this.workspaceRecommendations.activate(), + this.fileBasedRecommendations.activate(), + this.experimentalRecommendations.activate(), + this.keymapRecommendations.activate(), + this.lifecycleService.when(LifecyclePhase.Eventually) + .then(() => { + if (!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)) { + this.activateProactiveRecommendations(); + } + }) + ]); + + await this.promptWorkspaceRecommendations(); + this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations)(() => this.promptWorkspaceRecommendations())); + } + private isEnabled(): boolean { return this.galleryService.isEnabled() && !this.environmentService.extensionDevelopmentLocationURI; } @@ -133,9 +163,12 @@ export class ExtensionRecommendationsService extends Disposable implements IExte return output; } - async getConfigBasedRecommendations(): Promise { + async getConfigBasedRecommendations(): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }> { await this.configBasedRecommendations.activate(); - return this.toExtensionRecommendations(this.configBasedRecommendations.recommendations); + return { + important: this.toExtensionRecommendations(this.configBasedRecommendations.importantRecommendations), + others: this.toExtensionRecommendations(this.configBasedRecommendations.otherRecommendations) + }; } async getOtherRecommendations(): Promise { @@ -191,6 +224,13 @@ export class ExtensionRecommendationsService extends Disposable implements IExte return this.toExtensionRecommendations(this.workspaceRecommendations.recommendations); } + async getExeBasedRecommendations(exe?: string): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }> { + await this.exeBasedRecommendations.activate(); + const { important, others } = exe ? this.exeBasedRecommendations.getRecommendations(exe) + : { important: this.exeBasedRecommendations.importantRecommendations, others: this.exeBasedRecommendations.otherRecommendations }; + return { important: this.toExtensionRecommendations(important), others: this.toExtensionRecommendations(others) }; + } + getFileBasedRecommendations(): IExtensionRecommendation[] { return this.toExtensionRecommendations(this.fileBasedRecommendations.recommendations); } @@ -254,6 +294,83 @@ export class ExtensionRecommendationsService extends Disposable implements IExte return allIgnoredRecommendations.indexOf(id.toLowerCase()) === -1; } + private async getInstallableExtensions(extensionIds: string[]): Promise { + const extensions: IExtension[] = []; + const pager = await this.extensionsWorkbenchService.queryGallery({ names: extensionIds, pageSize: extensionIds.length, source: 'install-recommendations' }, CancellationToken.None); + for (const extension of pager.firstPage) { + if (extension.gallery && (await this.extensionManagementService.canInstall(extension.gallery))) { + extensions.push(extension); + } + } + return extensions; + } + + private async promptWorkspaceRecommendations(): Promise { + const allowedRecommendations = [...this.workspaceRecommendations.recommendations, ...this.configBasedRecommendations.importantRecommendations] + .map(({ extensionId }) => extensionId) + .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); + + const config = this.configurationService.getValue(ConfigurationKey); + if (allowedRecommendations.length === 0 + || config.ignoreRecommendations || config.showRecommendationsOnlyOnDemand + || this.storageService.getBoolean(ignoreWorkspaceRecommendationsStorageKey, StorageScope.WORKSPACE, false)) { + return; + } + + let installed = await this.extensionManagementService.getInstalled(); + installed = installed.filter(l => this.extensionEnablementService.getEnablementState(l) !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind + const recommendations = allowedRecommendations.filter(extensionId => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier))); + + if (!recommendations.length) { + return; + } + + const extensions = await this.getInstallableExtensions(recommendations); + if (!extensions.length) { + return; + } + + const searchValue = '@recommended '; + this.notificationService.prompt( + Severity.Info, + localize('workspaceRecommended', "Do you want to install the recommended extensions for this repository?"), + [{ + label: localize('install', "Install"), + run: async () => { + this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'install' }); + await Promise.all(extensions.map(async extension => { + this.extensionsWorkbenchService.open(extension, { pinned: true }); + await this.extensionManagementService.installFromGallery(extension.gallery!); + })); + } + }, { + label: localize('showRecommendations', "Show Recommendations"), + run: async () => { + this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'show' }); + const action = this.instantiationService.createInstance(SearchExtensionsAction, searchValue); + try { + await action.run(); + } finally { + action.dispose(); + } + } + }, { + label: localize('neverShowAgain', "Don't Show Again"), + isSecondary: true, + run: () => { + this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'neverShowAgain' }); + this.storageService.store(ignoreWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE); + } + }], + { + sticky: true, + onCancel: () => { + this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'cancelled' }); + } + } + ); + } + private onDidStorageChange(e: IWorkspaceStorageChangeEvent): void { if (e.key === ignoredRecommendationsStorageKey && e.scope === StorageScope.GLOBAL && this.ignoredRecommendationsValue !== this.getStoredIgnoredRecommendationsValue() /* This checks if current window changed the value or not */) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index b6a43859392..5287eea642b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -15,11 +15,10 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWo import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/services/output/common/output'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID } from 'vs/workbench/contrib/extensions/common/extensions'; -import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; import { OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowPopularExtensionsAction, ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowDisabledExtensionsAction, ShowBuiltInExtensionsAction, UpdateAllAction, - EnableAllAction, EnableAllWorkspaceAction, DisableAllAction, DisableAllWorkspaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, ShowAzureExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, ConfigureRecommendedExtensionsCommandsContributor, InstallVSIXAction, ReinstallAction, InstallSpecificVersionOfExtensionAction, ClearExtensionsSearchResultsAction + EnableAllAction, EnableAllWorkspaceAction, DisableAllAction, DisableAllWorkspaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, ConfigureRecommendedExtensionsCommandsContributor, InstallVSIXAction, ReinstallAction, InstallSpecificVersionOfExtensionAction, ClearExtensionsSearchResultsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; @@ -55,7 +54,7 @@ import { MultiCommand } from 'vs/editor/browser/editorExtensions'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; // Singletons -registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); +// registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); // TODO@sandbox TODO@ben uncomment when 'semver-umd' can be loaded registerSingleton(IExtensionRecommendationsService, ExtensionRecommendationsService); Registry.as(OutputExtensions.OutputChannels) @@ -113,9 +112,6 @@ actionRegistry.registerWorkbenchAction(keymapRecommendationsActionDescriptor, 'P const languageExtensionsActionDescriptor = SyncActionDescriptor.from(ShowLanguageExtensionsAction); actionRegistry.registerWorkbenchAction(languageExtensionsActionDescriptor, 'Preferences: Language Extensions', PreferencesLabel); -const azureExtensionsActionDescriptor = SyncActionDescriptor.from(ShowAzureExtensionsAction); -actionRegistry.registerWorkbenchAction(azureExtensionsActionDescriptor, 'Preferences: Azure Extensions', PreferencesLabel); - const popularActionDescriptor = SyncActionDescriptor.from(ShowPopularExtensionsAction); actionRegistry.registerWorkbenchAction(popularActionDescriptor, 'Extensions: Show Popular Extensions', ExtensionsLabel); @@ -472,7 +468,7 @@ function overrideActionForActiveExtensionEditorWebview(command: MultiCommand | u const editorService = accessor.get(IEditorService); const editor = editorService.activeEditorPane; if (editor instanceof ExtensionEditor) { - if (editor.activeWebview) { + if (editor.activeWebview?.isFocused) { f(editor.activeWebview); return true; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.web.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.web.contribution.ts new file mode 100644 index 00000000000..619906cc521 --- /dev/null +++ b/src/vs/workbench/contrib/extensions/browser/extensions.web.contribution.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; + +// TODO@sandbox TODO@ben move back into common/extensions.contribution.ts when 'semver-umd' can be loaded +registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 16ca60f34a5..c773f250a3c 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -35,7 +35,6 @@ import { Color } from 'vs/base/common/color'; import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; import { ITextEditorSelection } from 'vs/platform/editor/common/editor'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { PagedModel } from 'vs/base/common/paging'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { MenuRegistry, MenuId, IMenuService } from 'vs/platform/actions/common/actions'; @@ -909,7 +908,7 @@ export class EnableForWorkspaceAction extends ExtensionAction { if (this.extension && this.extension.local) { this.enabled = this.extension.state === ExtensionState.Installed && !this.extensionEnablementService.isEnabled(this.extension.local) - && this.extensionEnablementService.canChangeEnablement(this.extension.local); + && this.extensionEnablementService.canChangeWorkspaceEnablement(this.extension.local); } } @@ -970,7 +969,7 @@ export class DisableForWorkspaceAction extends ExtensionAction { if (this.extension && this.extension.local && this.runningExtensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier) && this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY)) { this.enabled = this.extension.state === ExtensionState.Installed && (this.extension.enablementState === EnablementState.EnabledGlobally || this.extension.enablementState === EnablementState.EnabledWorkspace) - && this.extensionEnablementService.canChangeEnablement(this.extension.local); + && this.extensionEnablementService.canChangeWorkspaceEnablement(this.extension.local); } } @@ -1829,86 +1828,6 @@ export class ShowRecommendedExtensionsAction extends Action { } } -export class InstallRecommendedExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.installRecommendedExtensions'; - static readonly LABEL = localize('installRecommendedExtensions', "Install Recommended Extensions"); - - private _recommendations: string[] = []; - get recommendations(): string[] { return this._recommendations; } - set recommendations(recommendations: string[]) { this._recommendations = recommendations; this.enabled = this._recommendations.length > 0; } - - constructor( - id: string, - label: string, - recommendations: string[], - private readonly source: string, - @IViewletService private readonly viewletService: IViewletService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, - @IProductService private readonly productService: IProductService, - ) { - super(id, label, 'extension-action'); - this.recommendations = recommendations; - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@recommended '); - viewlet.focus(); - const names = this.recommendations; - return this.extensionWorkbenchService.queryGallery({ names, source: this.source }, CancellationToken.None).then(pager => { - let installPromises: Promise[] = []; - let model = new PagedModel(pager); - for (let i = 0; i < pager.total; i++) { - installPromises.push(model.resolve(i, CancellationToken.None).then(e => this.installExtension(e))); - } - return Promise.all(installPromises); - }); - }); - } - - private async installExtension(extension: IExtension): Promise { - try { - if (extension.local && extension.gallery) { - if (prefersExecuteOnUI(extension.local.manifest, this.productService, this.configurationService)) { - if (this.extensionManagementServerService.localExtensionManagementServer) { - await this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(extension.gallery); - return; - } - } else if (this.extensionManagementServerService.remoteExtensionManagementServer) { - await this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.installFromGallery(extension.gallery); - return; - } - } - await this.extensionWorkbenchService.install(extension); - } catch (err) { - console.error(err); - return promptDownloadManually(extension.gallery, localize('failedToInstall', "Failed to install \'{0}\'.", extension.identifier.id), err, this.instantiationService); - } - } -} - -export class InstallWorkspaceRecommendedExtensionsAction extends InstallRecommendedExtensionsAction { - - constructor( - recommendations: string[], - @IViewletService viewletService: IViewletService, - @IInstantiationService instantiationService: IInstantiationService, - @IExtensionsWorkbenchService extensionWorkbenchService: IExtensionsWorkbenchService, - @IConfigurationService configurationService: IConfigurationService, - @IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService, - @IProductService productService: IProductService, - ) { - super('workbench.extensions.action.installWorkspaceRecommendedExtensions', localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), recommendations, 'install-all-workspace-recommendations', - viewletService, instantiationService, extensionWorkbenchService, configurationService, extensionManagementServerService, productService); - } -} - export class ShowRecommendedExtensionAction extends Action { static readonly ID = 'workbench.extensions.action.showRecommendedExtension'; @@ -1921,7 +1840,7 @@ export class ShowRecommendedExtensionAction extends Action { @IViewletService private readonly viewletService: IViewletService, @IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService, ) { - super(InstallRecommendedExtensionAction.ID, InstallRecommendedExtensionAction.LABEL, undefined, false); + super(ShowRecommendedExtensionAction.ID, ShowRecommendedExtensionAction.LABEL, undefined, false); this.extensionId = extensionId; } @@ -2075,29 +1994,6 @@ export class ShowLanguageExtensionsAction extends Action { } } -export class ShowAzureExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showAzureExtensions'; - static readonly LABEL = localize('showAzureExtensionsShort', "Azure Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@sort:installs azure '); - viewlet.focus(); - }); - } -} - export class SearchCategoryAction extends Action { constructor( @@ -2110,12 +2006,23 @@ export class SearchCategoryAction extends Action { } run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search(`@category:"${this.category.toLowerCase()}"`); - viewlet.focus(); - }); + return new SearchExtensionsAction(`@category:"${this.category.toLowerCase()}"`, this.viewletService).run(); + } +} + +export class SearchExtensionsAction extends Action { + + constructor( + private readonly searchValue: string, + @IViewletService private readonly viewletService: IViewletService + ) { + super('extensions.searchExtensions', localize('search recommendations', "Search Extensions"), undefined, true); + } + + async run(): Promise { + const viewPaneContainer = (await this.viewletService.openViewlet(VIEWLET_ID, true))?.getViewPaneContainer() as IExtensionsViewPaneContainer; + viewPaneContainer.search(this.searchValue); + viewPaneContainer.focus(); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index a154ca87f76..33ab9f5a25e 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -59,6 +59,7 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr import { DragAndDropObserver } from 'vs/workbench/browser/dnd'; import { URI } from 'vs/base/common/uri'; import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; const NonEmptyWorkspaceContext = new RawContextKey('nonEmptyWorkspace', false); const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); @@ -127,14 +128,18 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio if (this.extensionManagementServerService.localExtensionManagementServer) { servers.push(this.extensionManagementServerService.localExtensionManagementServer); } + if (this.extensionManagementServerService.webExtensionManagementServer) { + servers.push(this.extensionManagementServerService.webExtensionManagementServer); + } if (this.extensionManagementServerService.remoteExtensionManagementServer) { servers.push(this.extensionManagementServerService.remoteExtensionManagementServer); } - if (servers.length === 0 && this.extensionManagementServerService.webExtensionManagementServer) { - servers.push(this.extensionManagementServerService.webExtensionManagementServer); - } const getViewName = (viewTitle: string, server: IExtensionManagementServer): string => { - return servers.length > 1 ? `${server.label} - ${viewTitle}` : viewTitle; + if (servers.length) { + const serverLabel = server === this.extensionManagementServerService.webExtensionManagementServer && !this.extensionManagementServerService.localExtensionManagementServer ? localize('local', "Local") : server.label; + return servers.length > 1 ? `${serverLabel} - ${viewTitle}` : viewTitle; + } + return viewTitle; }; for (const server of servers) { const getInstalledViewName = (): string => getViewName(localize('installed', "Installed"), server); @@ -349,6 +354,8 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE @IInstantiationService instantiationService: IInstantiationService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @INotificationService private readonly notificationService: INotificationService, @IViewletService private readonly viewletService: IViewletService, @@ -519,14 +526,19 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE ]); if (this.extensionGalleryService.isEnabled()) { - filterActions.splice(0, 0, ...[ + const galleryFilterActions = [ this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.featured', localize('featured filter', "Featured"), '@featured'), this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.popular', localize('most popular filter', "Most Popular"), '@popular'), this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.recommended', localize('most popular recommended', "Recommended"), '@recommended'), this.instantiationService.createInstance(RecentlyPublishedExtensionsAction, RecentlyPublishedExtensionsAction.ID, localize('recently published filter', "Recently Published")), + new Separator(), new SubmenuAction('workbench.extensions.action.filterExtensionsByCategory', localize('filter by category', "Category"), EXTENSION_CATEGORIES.map(category => this.instantiationService.createInstance(SearchCategoryAction, `extensions.actions.searchByCategory.${category}`, category, category))), new Separator(), - ]); + ]; + if (this.extensionManagementServerService.webExtensionManagementServer || !this.environmentService.isBuilt) { + galleryFilterActions.splice(4, 0, this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.web', localize('web filter', "Web"), '@web')); + } + filterActions.splice(0, 0, ...galleryFilterActions); filterActions.push(...[ new Separator(), new SubmenuAction('workbench.extensions.action.sortBy', localize('sorty by', "Sort By"), this.sortActions), @@ -580,6 +592,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE .replace(/@tag:/g, 'tag:') .replace(/@ext:/g, 'ext:') .replace(/@featured/g, 'featured') + .replace(/@web/g, 'tag:"__web_extension"') .replace(/@popular/g, '@sort:installs') : ''; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 8e34bdaf743..dae49c0bbca 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -9,7 +9,7 @@ import { assign } from 'vs/base/common/objects'; import { Event, Emitter } from 'vs/base/common/event'; import { isPromiseCanceledError, getErrorMessage } from 'vs/base/common/errors'; import { PagedModel, IPagedModel, IPager, DelayedPagedModel } from 'vs/base/common/paging'; -import { SortBy, SortOrder, IQueryOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { SortBy, SortOrder, IQueryOptions, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManagementServer, IExtensionManagementServerService, IExtensionRecommendationsService, IExtensionRecommendation, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -17,7 +17,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { append, $, toggleClass, addClass } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Delegate, Renderer, IExtensionsViewState } from 'vs/workbench/contrib/extensions/browser/extensionsList'; -import { IExtension, IExtensionsWorkbenchService, ExtensionState } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -25,13 +25,13 @@ import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; -import { InstallWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction, InstallLocalExtensionsInRemoteAction, getContextMenuActions, ExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction, InstallLocalExtensionsInRemoteAction, getContextMenuActions, ExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { WorkbenchPagedList, ListResourceNavigator } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { distinct, coalesce } from 'vs/base/common/arrays'; +import { coalesce, distinct, flatten } from 'vs/base/common/arrays'; import { IExperimentService, IExperiment, ExperimentActionType } from 'vs/workbench/contrib/experiments/common/experimentService'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; @@ -53,6 +53,10 @@ import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; // Extensions that are automatically classified as Programming Language extensions, but should be Feature extensions const FORCE_FEATURE_EXTENSIONS = ['vscode.git', 'vscode.search-result']; +type WorkspaceRecommendationsClassification = { + count: { classification: 'SystemMetaData', purpose: 'FeatureInsight', 'isMeasurement': true }; +}; + class ExtensionsViewState extends Disposable implements IExtensionsViewState { private readonly _onFocus: Emitter = this._register(new Emitter()); @@ -98,12 +102,13 @@ export class ExtensionsListView extends ViewPane { @IThemeService themeService: IThemeService, @IExtensionService private readonly extensionService: IExtensionService, @IExtensionsWorkbenchService protected extensionsWorkbenchService: IExtensionsWorkbenchService, - @IExtensionRecommendationsService protected tipsService: IExtensionRecommendationsService, + @IExtensionRecommendationsService protected extensionRecommendationsService: IExtensionRecommendationsService, @ITelemetryService telemetryService: ITelemetryService, @IConfigurationService configurationService: IConfigurationService, @IWorkspaceContextService protected contextService: IWorkspaceContextService, @IExperimentService private readonly experimentService: IExperimentService, @IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService, + @IExtensionManagementService protected readonly extensionManagementService: IExtensionManagementService, @IProductService protected readonly productService: IProductService, @IContextKeyService contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @@ -456,14 +461,8 @@ export class ExtensionsListView extends ViewPane { options.sortBy = SortBy.InstallCount; } - if (ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)) { - return this.getWorkspaceRecommendationsModel(query, options, token); - } else if (ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)) { - return this.getKeymapRecommendationsModel(query, options, token); - } else if (/@recommended:all/i.test(query.value) || ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value)) { - return this.getAllRecommendationsModel(query, options, token); - } else if (ExtensionsListView.isRecommendedExtensionsQuery(query.value)) { - return this.getRecommendationsModel(query, options, token); + if (this.isRecommendationsQuery(query)) { + return this.queryRecommendations(query, options, token); } if (/\bcurated:([^\s]+)\b/.test(query.value)) { @@ -540,51 +539,6 @@ export class ExtensionsListView extends ViewPane { return extensions; } - // Get All types of recommendations, trimmed to show a max of 8 at any given time - private getAllRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { - const value = query.value.replace(/@recommended:all/g, '').replace(/@recommended/g, '').trim().toLowerCase(); - - return this.extensionsWorkbenchService.queryLocal(this.server) - .then(result => result.filter(e => e.type === ExtensionType.User)) - .then(local => { - const fileBasedRecommendations = this.tipsService.getFileBasedRecommendations(); - const configBasedRecommendationsPromise = this.tipsService.getConfigBasedRecommendations(); - const othersPromise = this.tipsService.getOtherRecommendations(); - const workspacePromise = this.tipsService.getWorkspaceRecommendations(); - const importantRecommendationsPromise = this.tipsService.getImportantRecommendations(); - - return Promise.all([othersPromise, workspacePromise, configBasedRecommendationsPromise, importantRecommendationsPromise]) - .then(([others, workspaceRecommendations, configBasedRecommendations, importantRecommendations]) => { - const names = this.getTrimmedRecommendations(local, value, importantRecommendations, fileBasedRecommendations, configBasedRecommendations, others, workspaceRecommendations); - const recommendationsWithReason = this.tipsService.getAllRecommendationsWithReason(); - /* __GDPR__ - "extensionAllRecommendations:open" : { - "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "recommendations": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('extensionAllRecommendations:open', { - count: names.length, - recommendations: names.map(id => { - return { - id, - recommendationReason: recommendationsWithReason[id.toLowerCase()].reasonId - }; - }) - }); - if (!names.length) { - return Promise.resolve(new PagedModel([])); - } - options.source = 'recommendations-all'; - return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token) - .then(pager => { - this.sortFirstPage(pager, names); - return this.getPagedModel(pager || []); - }); - }); - }); - } - private async getCuratedModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { const value = query.value.replace(/curated:/g, '').trim(); const names = await this.experimentService.getCuratedExtensionsList(value); @@ -597,139 +551,138 @@ export class ExtensionsListView extends ViewPane { return new PagedModel([]); } - // Get All types of recommendations other than Workspace recommendations, trimmed to show a max of 8 at any given time - private getRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + private isRecommendationsQuery(query: Query): boolean { + return ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value) + || ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value) + || ExtensionsListView.isExeRecommendedExtensionsQuery(query.value) + || /@recommended:all/i.test(query.value) + || ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value) + || ExtensionsListView.isRecommendedExtensionsQuery(query.value); + } + + private async queryRecommendations(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + // Workspace recommendations + if (ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)) { + return this.getWorkspaceRecommendationsModel(query, options, token); + } + + // Keymap recommendations + if (ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)) { + return this.getKeymapRecommendationsModel(query, options, token); + } + + // Exe recommendations + if (ExtensionsListView.isExeRecommendedExtensionsQuery(query.value)) { + return this.getExeRecommendationsModel(query, options, token); + } + + // All recommendations + if (/@recommended:all/i.test(query.value) || ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value)) { + return this.getAllRecommendationsModel(query, options, token); + } + + // Other recommendations + if (ExtensionsListView.isRecommendedExtensionsQuery(query.value)) { + return this.getOtherRecommendationsModel(query, options, token); + } + + return new PagedModel([]); + } + + protected async getInstallableRecommendations(recommendations: IExtensionRecommendation[], options: IQueryOptions, token: CancellationToken): Promise { + const extensions: IExtension[] = []; + if (recommendations.length) { + const names = recommendations.map(({ extensionId }) => extensionId); + const pager = await this.extensionsWorkbenchService.queryGallery({ ...options, names, pageSize: names.length }, token); + for (const extension of pager.firstPage) { + if (extension.gallery && (await this.extensionManagementService.canInstall(extension.gallery))) { + extensions.push(extension); + } + } + } + return extensions; + } + + protected async getWorkspaceRecommendations(): Promise { + const recommendations = await this.extensionRecommendationsService.getWorkspaceRecommendations(); + const { important } = await this.extensionRecommendationsService.getConfigBasedRecommendations(); + for (const configBasedRecommendation of important) { + if (recommendations.some(r => r.extensionId !== configBasedRecommendation.extensionId)) { + recommendations.push(configBasedRecommendation); + } + } + return recommendations; + } + + private async getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + const value = query.value.replace(/@recommended:workspace/g, '').trim().toLowerCase(); + const recommendations = await this.getWorkspaceRecommendations(); + const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-workspace' }, token)) + .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); + this.telemetryService.publicLog2<{ count: number }, WorkspaceRecommendationsClassification>('extensionWorkspaceRecommendations:open', { count: installableRecommendations.length }); + const result: IExtension[] = coalesce(recommendations.map(({ extensionId: id }) => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); + return new PagedModel(result); + } + + private async getKeymapRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + const value = query.value.replace(/@recommended:keymaps/g, '').trim().toLowerCase(); + const recommendations = this.extensionRecommendationsService.getKeymapRecommendations(); + const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-keymaps' }, token)) + .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); + return new PagedModel(installableRecommendations); + } + + private async getExeRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + const exe = query.value.replace(/@exe:/g, '').trim().toLowerCase(); + const { important, others } = await this.extensionRecommendationsService.getExeBasedRecommendations(exe.startsWith('"') ? exe.substring(1, exe.length - 1) : exe); + const installableRecommendations = await this.getInstallableRecommendations([...important, ...others], { ...options, source: 'recommendations-exe' }, token); + return new PagedModel(installableRecommendations); + } + + private async getOtherRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { const value = query.value.replace(/@recommended/g, '').trim().toLowerCase(); - return this.extensionsWorkbenchService.queryLocal(this.server) - .then(result => result.filter(e => e.type === ExtensionType.User)) - .then(local => { - let fileBasedRecommendations = this.tipsService.getFileBasedRecommendations(); - const configBasedRecommendationsPromise = this.tipsService.getConfigBasedRecommendations(); - const othersPromise = this.tipsService.getOtherRecommendations(); - const workspacePromise = this.tipsService.getWorkspaceRecommendations(); - const importantRecommendationsPromise = this.tipsService.getImportantRecommendations(); + const local = (await this.extensionsWorkbenchService.queryLocal(this.server)) + .filter(e => e.type === ExtensionType.User) + .map(e => e.identifier.id.toLowerCase()); + const workspaceRecommendations = (await this.getWorkspaceRecommendations()) + .map(r => r.extensionId.toLowerCase()); - return Promise.all([othersPromise, workspacePromise, configBasedRecommendationsPromise, importantRecommendationsPromise]) - .then(([others, workspaceRecommendations, configBasedRecommendations, importantRecommendations]) => { - configBasedRecommendations = configBasedRecommendations.filter(x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId)); - fileBasedRecommendations = fileBasedRecommendations.filter(x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId)); - others = others.filter(x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId)); + const otherRecommendations = distinct( + flatten(await Promise.all([ + // Order is important + this.extensionRecommendationsService.getImportantRecommendations(), + this.extensionRecommendationsService.getFileBasedRecommendations(), + this.extensionRecommendationsService.getOtherRecommendations() + ])).filter(({ extensionId }) => !local.includes(extensionId.toLowerCase()) && !workspaceRecommendations.includes(extensionId.toLowerCase()) + ), r => r.extensionId.toLowerCase()); - const names = this.getTrimmedRecommendations(local, value, importantRecommendations, fileBasedRecommendations, configBasedRecommendations, others, []); - const recommendationsWithReason = this.tipsService.getAllRecommendationsWithReason(); + const installableRecommendations = (await this.getInstallableRecommendations(otherRecommendations, { ...options, source: 'recommendations-other', sortBy: undefined }, token)) + .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); - /* __GDPR__ - "extensionRecommendations:open" : { - "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "recommendations": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('extensionRecommendations:open', { - count: names.length, - recommendations: names.map(id => { - return { - id, - recommendationReason: recommendationsWithReason[id.toLowerCase()].reasonId - }; - }) - }); - - if (!names.length) { - return Promise.resolve(new PagedModel([])); - } - options.source = 'recommendations'; - return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token) - .then(pager => { - this.sortFirstPage(pager, names); - return this.getPagedModel(pager || []); - }); - }); - }); + const result: IExtension[] = coalesce(otherRecommendations.map(({ extensionId: id }) => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); + return new PagedModel(result); } - // Given all recommendations, trims and returns recommendations in the relevant order after filtering out installed extensions - private getTrimmedRecommendations(installedExtensions: IExtension[], value: string, importantRecommendations: IExtensionRecommendation[], fileBasedRecommendations: IExtensionRecommendation[], configBasedRecommendations: IExtensionRecommendation[], otherRecommendations: IExtensionRecommendation[], workspaceRecommendations: IExtensionRecommendation[]): string[] { - const totalCount = 10; - workspaceRecommendations = workspaceRecommendations - .filter(recommendation => { - return !this.isRecommendationInstalled(recommendation, installedExtensions) - && recommendation.extensionId.toLowerCase().indexOf(value) > -1; - }); - importantRecommendations = importantRecommendations - .filter(recommendation => { - return !this.isRecommendationInstalled(recommendation, installedExtensions) - && workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId) - && recommendation.extensionId.toLowerCase().indexOf(value) > -1; - }); - configBasedRecommendations = configBasedRecommendations - .filter(recommendation => { - return !this.isRecommendationInstalled(recommendation, installedExtensions) - && workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId) - && importantRecommendations.every(importantRecommendation => importantRecommendation.extensionId !== recommendation.extensionId) - && recommendation.extensionId.toLowerCase().indexOf(value) > -1; - }); - fileBasedRecommendations = fileBasedRecommendations.filter(recommendation => { - return !this.isRecommendationInstalled(recommendation, installedExtensions) - && workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId) - && importantRecommendations.every(importantRecommendation => importantRecommendation.extensionId !== recommendation.extensionId) - && configBasedRecommendations.every(configBasedRecommendation => configBasedRecommendation.extensionId !== recommendation.extensionId) - && recommendation.extensionId.toLowerCase().indexOf(value) > -1; - }); - otherRecommendations = otherRecommendations.filter(recommendation => { - return !this.isRecommendationInstalled(recommendation, installedExtensions) - && fileBasedRecommendations.every(fileBasedRecommendation => fileBasedRecommendation.extensionId !== recommendation.extensionId) - && workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId) - && importantRecommendations.every(importantRecommendation => importantRecommendation.extensionId !== recommendation.extensionId) - && configBasedRecommendations.every(configBasedRecommendation => configBasedRecommendation.extensionId !== recommendation.extensionId) - && recommendation.extensionId.toLowerCase().indexOf(value) > -1; - }); + // Get All types of recommendations, trimmed to show a max of 8 at any given time + private async getAllRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + const local = (await this.extensionsWorkbenchService.queryLocal(this.server)) + .filter(e => e.type === ExtensionType.User) + .map(e => e.identifier.id.toLowerCase()); - const otherCount = Math.min(2, otherRecommendations.length); - const fileBasedCount = Math.min(fileBasedRecommendations.length, totalCount - workspaceRecommendations.length - importantRecommendations.length - configBasedRecommendations.length - otherCount); - const recommendations = [...workspaceRecommendations, ...importantRecommendations, ...configBasedRecommendations]; - recommendations.push(...fileBasedRecommendations.splice(0, fileBasedCount)); - recommendations.push(...otherRecommendations.splice(0, otherCount)); + const allRecommendations = distinct( + flatten(await Promise.all([ + // Order is important + this.getWorkspaceRecommendations(), + this.extensionRecommendationsService.getImportantRecommendations(), + this.extensionRecommendationsService.getFileBasedRecommendations(), + this.extensionRecommendationsService.getOtherRecommendations() + ])).filter(({ extensionId }) => !local.includes(extensionId.toLowerCase()) + ), r => r.extensionId.toLowerCase()); - return distinct(recommendations.map(({ extensionId }) => extensionId)); - } - - private isRecommendationInstalled(recommendation: IExtensionRecommendation, installed: IExtension[]): boolean { - return installed.some(i => areSameExtensions(i.identifier, { id: recommendation.extensionId })); - } - - private getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { - const value = query.value.replace(/@recommended:workspace/g, '').trim().toLowerCase(); - return this.tipsService.getWorkspaceRecommendations() - .then(recommendations => { - const names = recommendations.map(({ extensionId }) => extensionId).filter(name => name.toLowerCase().indexOf(value) > -1); - /* __GDPR__ - "extensionWorkspaceRecommendations:open" : { - "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } - } - */ - this.telemetryService.publicLog('extensionWorkspaceRecommendations:open', { count: names.length }); - - if (!names.length) { - return Promise.resolve(new PagedModel([])); - } - options.source = 'recommendations-workspace'; - return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token) - .then(pager => this.getPagedModel(pager || [])); - }); - } - - private getKeymapRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { - const value = query.value.replace(/@recommended:keymaps/g, '').trim().toLowerCase(); - const names: string[] = this.tipsService.getKeymapRecommendations().map(({ extensionId }) => extensionId) - .filter(extensionId => extensionId.toLowerCase().indexOf(value) > -1); - - if (!names.length) { - return Promise.resolve(new PagedModel([])); - } - options.source = 'recommendations-keymaps'; - return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token) - .then(result => this.getPagedModel(result)); + const installableRecommendations = await this.getInstallableRecommendations(allRecommendations, { ...options, source: 'recommendations-all', sortBy: undefined }, token); + const result: IExtension[] = coalesce(allRecommendations.map(({ extensionId: id }) => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); + return new PagedModel(result.slice(0, 8)); } // Sorts the firstPage of the pager in the same order as given array of extension ids @@ -864,6 +817,10 @@ export class ExtensionsListView extends ViewPane { return /@recommended:workspace/i.test(query); } + static isExeRecommendedExtensionsQuery(query: string): boolean { + return /@exe:.+/i.test(query); + } + static isKeymapsRecommendedExtensionsQuery(query: string): boolean { return /@recommended:keymaps/i.test(query); } @@ -900,6 +857,7 @@ export class ServerExtensionsView extends ExtensionsListView { @IExperimentService experimentService: IExperimentService, @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService, @IProductService productService: IProductService, @IContextKeyService contextKeyService: IContextKeyService, @IMenuService menuService: IMenuService, @@ -908,7 +866,9 @@ export class ServerExtensionsView extends ExtensionsListView { @IPreferencesService preferencesService: IPreferencesService, ) { options.server = server; - super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, tipsService, telemetryService, configurationService, contextService, experimentService, extensionManagementServerService, productService, contextKeyService, viewDescriptorService, menuService, openerService, preferencesService); + super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, tipsService, + telemetryService, configurationService, contextService, experimentService, extensionManagementServerService, extensionManagementService, productService, + contextKeyService, viewDescriptorService, menuService, openerService, preferencesService); this._register(onDidChangeTitle(title => this.updateTitle(title))); } @@ -992,7 +952,7 @@ export class DefaultRecommendedExtensionsView extends ExtensionsListView { renderBody(container: HTMLElement): void { super.renderBody(container); - this._register(this.tipsService.onRecommendationChange(() => { + this._register(this.extensionRecommendationsService.onRecommendationChange(() => { this.show(''); })); } @@ -1017,7 +977,7 @@ export class RecommendedExtensionsView extends ExtensionsListView { renderBody(container: HTMLElement): void { super.renderBody(container); - this._register(this.tipsService.onRecommendationChange(() => { + this._register(this.extensionRecommendationsService.onRecommendationChange(() => { this.show(''); })); } @@ -1029,20 +989,18 @@ export class RecommendedExtensionsView extends ExtensionsListView { export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { private readonly recommendedExtensionsQuery = '@recommended:workspace'; - private installAllAction: InstallWorkspaceRecommendedExtensionsAction | undefined; + private installAllAction: Action | undefined; renderBody(container: HTMLElement): void { super.renderBody(container); - this._register(this.tipsService.onRecommendationChange(() => this.update())); - this._register(this.extensionsWorkbenchService.onChange(() => this.setRecommendationsToInstall())); - this._register(this.contextService.onDidChangeWorkbenchState(() => this.update())); + this._register(this.extensionRecommendationsService.onRecommendationChange(() => this.show(this.recommendedExtensionsQuery))); + this._register(this.contextService.onDidChangeWorkbenchState(() => this.show(this.recommendedExtensionsQuery))); } getActions(): IAction[] { if (!this.installAllAction) { - this.installAllAction = this._register(this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, [])); - this.installAllAction.class = 'codicon codicon-cloud-download'; + this.installAllAction = this._register(new Action('workbench.extensions.action.installWorkspaceRecommendedExtensions', localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), 'codicon codicon-cloud-download', false, () => this.installWorkspaceRecommendations())); } const configureWorkspaceFolderAction = this._register(this.instantiationService.createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL)); @@ -1054,33 +1012,28 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { let shouldShowEmptyView = query && query.trim() !== '@recommended' && query.trim() !== '@recommended:workspace'; let model = await (shouldShowEmptyView ? this.showEmptyModel() : super.show(this.recommendedExtensionsQuery)); this.setExpanded(model.length > 0); + await this.setRecommendationsToInstall(); return model; } - private update(): void { - this.show(this.recommendedExtensionsQuery); - this.setRecommendationsToInstall(); - } - private async setRecommendationsToInstall(): Promise { - const recommendations = await this.getRecommendationsToInstall(); + const installableRecommendations = await this.getInstallableWorkspaceRecommendations(); if (this.installAllAction) { - this.installAllAction.recommendations = recommendations.map(({ extensionId }) => extensionId); + this.installAllAction.enabled = installableRecommendations.length > 0; } } - private getRecommendationsToInstall(): Promise { - return this.tipsService.getWorkspaceRecommendations() - .then(recommendations => recommendations.filter(({ extensionId }) => { - const extension = this.extensionsWorkbenchService.local.filter(i => areSameExtensions({ id: extensionId }, i.identifier))[0]; - if (!extension - || !extension.local - || extension.state !== ExtensionState.Installed - || extension.enablementState === EnablementState.DisabledByExtensionKind - ) { - return true; - } - return false; - })); + private async getInstallableWorkspaceRecommendations() { + const installed = (await this.extensionsWorkbenchService.queryLocal()) + .filter(l => l.enablementState !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind + const recommendations = (await this.getWorkspaceRecommendations()) + .filter(({ extensionId }) => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier))); + return this.getInstallableRecommendations(recommendations, { source: 'install-all-workspace-recommendations' }, CancellationToken.None); } + + private async installWorkspaceRecommendations(): Promise { + const installableRecommendations = await this.getInstallableWorkspaceRecommendations(); + await Promise.all(installableRecommendations.map(extension => this.extensionManagementService.installFromGallery(extension.gallery!))); + } + } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 8a7c80af7c1..c5758eac3d6 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -55,7 +55,7 @@ export class InstallCountWidget extends ExtensionWidget { } render(): void { - this.container.innerHTML = ''; + this.container.innerText = ''; if (!this.extension) { return; @@ -105,7 +105,7 @@ export class RatingsWidget extends ExtensionWidget { } render(): void { - this.container.innerHTML = ''; + this.container.innerText = ''; if (!this.extension) { return; diff --git a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts index fcc7c701ba1..aafed5a313f 100644 --- a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts @@ -13,11 +13,11 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { localize } from 'vs/nls'; import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IProductService } from 'vs/platform/product/common/productService'; +import { ImportantExtensionTip, IProductService } from 'vs/platform/product/common/productService'; import { forEach, IStringDictionary } from 'vs/base/common/collections'; import { ITextModel } from 'vs/editor/common/model'; import { Schemas } from 'vs/base/common/network'; -import { extname } from 'vs/base/common/resources'; +import { basename, extname } from 'vs/base/common/resources'; import { match } from 'vs/base/common/glob'; import { URI } from 'vs/base/common/uri'; import { MIME_UNKNOWN, guessMimeTypes } from 'vs/base/common/mime'; @@ -26,6 +26,8 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { setImmediate } from 'vs/base/common/platform'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IModeService } from 'vs/editor/common/services/modeService'; type FileExtensionSuggestionClassification = { userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; @@ -35,32 +37,34 @@ type FileExtensionSuggestionClassification = { const recommendationsStorageKey = 'extensionsAssistant/recommendations'; const searchMarketplace = localize('searchMarketplace', "Search Marketplace"); const milliSecondsInADay = 1000 * 60 * 60 * 24; -const processedFileExtensions: string[] = []; export class FileBasedRecommendations extends ExtensionRecommendations { - private readonly extensionTips: IStringDictionary = Object.create(null); - private readonly importantExtensionTips: IStringDictionary<{ name: string; pattern: string; isExtensionPack?: boolean }> = Object.create(null); + private readonly extensionTips = new Map(); + private readonly importantExtensionTips = new Map(); - private fileBasedRecommendationsByPattern: IStringDictionary = Object.create(null); - private fileBasedRecommendations: IStringDictionary<{ recommendedTime: number, sources: ExtensionRecommendationSource[] }> = Object.create(null); + private readonly fileBasedRecommendationsByPattern = new Map(); + private readonly fileBasedRecommendationsByLanguage = new Map(); + private readonly fileBasedRecommendations = new Map(); + private readonly processedFileExtensions: string[] = []; + private readonly processedLanguages: string[] = []; get recommendations(): ReadonlyArray { const recommendations: ExtensionRecommendation[] = []; - Object.keys(this.fileBasedRecommendations) + [...this.fileBasedRecommendations.keys()] .sort((a, b) => { - if (this.fileBasedRecommendations[a].recommendedTime === this.fileBasedRecommendations[b].recommendedTime) { - if (this.importantExtensionTips[a]) { + if (this.fileBasedRecommendations.get(a)!.recommendedTime === this.fileBasedRecommendations.get(b)!.recommendedTime) { + if (this.importantExtensionTips.has(a)) { return -1; } - if (this.importantExtensionTips[b]) { + if (this.importantExtensionTips.has(b)) { return 1; } } - return this.fileBasedRecommendations[a].recommendedTime > this.fileBasedRecommendations[b].recommendedTime ? -1 : 1; + return this.fileBasedRecommendations.get(a)!.recommendedTime > this.fileBasedRecommendations.get(b)!.recommendedTime ? -1 : 1; }) .forEach(extensionId => { - for (const source of this.fileBasedRecommendations[extensionId].sources) { + for (const source of this.fileBasedRecommendations.get(extensionId)!.sources) { recommendations.push({ extensionId, source, @@ -75,19 +79,21 @@ export class FileBasedRecommendations extends ExtensionRecommendations { } get importantRecommendations(): ReadonlyArray { - return this.recommendations.filter(e => this.importantExtensionTips[e.extensionId]); + return this.recommendations.filter(e => this.importantExtensionTips.has(e.extensionId)); } get otherRecommendations(): ReadonlyArray { - return this.recommendations.filter(e => !this.importantExtensionTips[e.extensionId]); + return this.recommendations.filter(e => !this.importantExtensionTips.has(e.extensionId)); } constructor( isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService, @IExtensionService private readonly extensionService: IExtensionService, @IViewletService private readonly viewletService: IViewletService, @IModelService private readonly modelService: IModelService, + @IModeService private readonly modeService: IModeService, @IProductService productService: IProductService, @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @@ -96,32 +102,43 @@ export class FileBasedRecommendations extends ExtensionRecommendations { @IStorageService storageService: IStorageService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, extensionsWorkbenchService, extensionManagementService, storageKeysSyncRegistryService); if (productService.extensionTips) { - forEach(productService.extensionTips, ({ key, value }) => this.extensionTips[key.toLowerCase()] = value); + forEach(productService.extensionTips, ({ key, value }) => this.extensionTips.set(key.toLowerCase(), value)); } if (productService.extensionImportantTips) { - forEach(productService.extensionImportantTips, ({ key, value }) => this.importantExtensionTips[key.toLowerCase()] = value); + forEach(productService.extensionImportantTips, ({ key, value }) => this.importantExtensionTips.set(key.toLowerCase(), value)); } } protected async doActivate(): Promise { + await this.extensionService.whenInstalledExtensionsRegistered(); + const allRecommendations: string[] = []; // group extension recommendations by pattern, like {**/*.md} -> [ext.foo1, ext.bar2] - forEach(this.extensionTips, ({ key: extensionId, value: pattern }) => { - const ids = this.fileBasedRecommendationsByPattern[pattern] || []; + for (const [extensionId, pattern] of this.extensionTips) { + const ids = this.fileBasedRecommendationsByPattern.get(pattern) || []; ids.push(extensionId); - this.fileBasedRecommendationsByPattern[pattern] = ids; + this.fileBasedRecommendationsByPattern.set(pattern, ids); allRecommendations.push(extensionId); - }); - forEach(this.importantExtensionTips, ({ key: extensionId, value }) => { - const ids = this.fileBasedRecommendationsByPattern[value.pattern] || []; - ids.push(extensionId); - this.fileBasedRecommendationsByPattern[value.pattern] = ids; + } + for (const [extensionId, value] of this.importantExtensionTips) { + if (value.pattern) { + const ids = this.fileBasedRecommendationsByPattern.get(value.pattern) || []; + ids.push(extensionId); + this.fileBasedRecommendationsByPattern.set(value.pattern, ids); + } + if (value.languages) { + for (const language of value.languages) { + const ids = this.fileBasedRecommendationsByLanguage.get(language) || []; + ids.push(extensionId); + this.fileBasedRecommendationsByLanguage.set(language, ids); + } + } allRecommendations.push(extensionId); - }); + } const cachedRecommendations = this.getCachedRecommendations(); const now = Date.now(); @@ -129,12 +146,17 @@ export class FileBasedRecommendations extends ExtensionRecommendations { forEach(cachedRecommendations, ({ key, value }) => { const diff = (now - value) / milliSecondsInADay; if (diff <= 7 && allRecommendations.indexOf(key) > -1) { - this.fileBasedRecommendations[key] = { recommendedTime: value, sources: ['cached'] }; + this.fileBasedRecommendations.set(key.toLowerCase(), { recommendedTime: value, sources: ['cached'] }); } }); - this._register(this.modelService.onModelAdded(this.promptRecommendationsForModel, this)); - this.modelService.getModels().forEach(model => this.promptRecommendationsForModel(model)); + this._register(this.modelService.onModelAdded(model => this.onModelAdded(model))); + this.modelService.getModels().forEach(model => this.onModelAdded(model)); + } + + private onModelAdded(model: ITextModel): void { + this.promptRecommendationsForModel(model); + this._register(model.onDidChangeLanguage(() => this.promptRecommendationsForModel(model))); } /** @@ -144,43 +166,54 @@ export class FileBasedRecommendations extends ExtensionRecommendations { private promptRecommendationsForModel(model: ITextModel): void { const uri = model.uri; const supportedSchemes = [Schemas.untitled, Schemas.file, Schemas.vscodeRemote]; - if (!uri || supportedSchemes.indexOf(uri.scheme) === -1) { + if (!uri || !supportedSchemes.includes(uri.scheme)) { return; } - let fileExtension = extname(uri); - if (fileExtension) { - if (processedFileExtensions.indexOf(fileExtension) > -1) { - return; - } - processedFileExtensions.push(fileExtension); + const language = model.getLanguageIdentifier().language; + const fileExtension = extname(uri); + if (this.processedLanguages.includes(language) && this.processedFileExtensions.includes(fileExtension)) { + return; } + this.processedLanguages.push(language); + this.processedFileExtensions.push(fileExtension); + // re-schedule this bit of the operation to be off the critical path - in case glob-match is slow - setImmediate(() => this.promptRecommendations(uri, fileExtension)); + setImmediate(() => this.promptRecommendations(uri, language, fileExtension)); } - private async promptRecommendations(uri: URI, fileExtension: string): Promise { - const recommendationsToPrompt: string[] = []; - forEach(this.fileBasedRecommendationsByPattern, ({ key: pattern, value: extensionIds }) => { - if (match(pattern, uri.toString())) { - for (const extensionId of extensionIds) { - // Add to recommendation to prompt if it is an important tip - // Only prompt if the pattern matches the extensionImportantTips pattern - // Otherwise, assume pattern is from extensionTips, which means it should be a file based "passive" recommendation - if (this.importantExtensionTips[extensionId]?.pattern === pattern) { - recommendationsToPrompt.push(extensionId); - } - // Update file based recommendations - const filedBasedRecommendation = this.fileBasedRecommendations[extensionId] || { recommendedTime: Date.now(), sources: [] }; - filedBasedRecommendation.recommendedTime = Date.now(); - if (!filedBasedRecommendation.sources.some(s => s instanceof URI && s.toString() === uri.toString())) { - filedBasedRecommendation.sources.push(uri); - } - this.fileBasedRecommendations[extensionId.toLowerCase()] = filedBasedRecommendation; + private async promptRecommendations(uri: URI, language: string, fileExtension: string): Promise { + const importantRecommendations: string[] = (this.fileBasedRecommendationsByLanguage.get(language) || []).filter(extensionId => this.importantExtensionTips.has(extensionId)); + let languageName: string | null = importantRecommendations.length ? this.modeService.getLanguageName(language) : null; + + const fileBasedRecommendations: string[] = [...importantRecommendations]; + for (let [pattern, extensionIds] of this.fileBasedRecommendationsByPattern) { + extensionIds = extensionIds.filter(extensionId => !importantRecommendations.includes(extensionId)); + if (!extensionIds.length) { + continue; + } + if (!match(pattern, uri.toString())) { + continue; + } + for (const extensionId of extensionIds) { + fileBasedRecommendations.push(extensionId); + const importantExtensionTip = this.importantExtensionTips.get(extensionId); + if (importantExtensionTip && importantExtensionTip.pattern === pattern) { + importantRecommendations.push(extensionId); } } - }); + } + + // Update file based recommendations + for (const recommendation of fileBasedRecommendations) { + const filedBasedRecommendation = this.fileBasedRecommendations.get(recommendation) || { recommendedTime: Date.now(), sources: [] }; + filedBasedRecommendation.recommendedTime = Date.now(); + if (!filedBasedRecommendation.sources.some(s => s instanceof URI && s.toString() === uri.toString())) { + filedBasedRecommendation.sources.push(uri); + } + this.fileBasedRecommendations.set(recommendation, filedBasedRecommendation); + } this.storeCachedRecommendations(); @@ -189,18 +222,16 @@ export class FileBasedRecommendations extends ExtensionRecommendations { } const installed = await this.extensionsWorkbenchService.queryLocal(); - if (await this.promptRecommendedExtensionForFileType(recommendationsToPrompt, installed)) { + if (importantRecommendations.length && + await this.promptRecommendedExtensionForFileType(languageName || basename(uri), importantRecommendations, installed)) { return; } - if (fileExtension) { - fileExtension = fileExtension.substr(1); // Strip the dot - } + fileExtension = fileExtension.substr(1); // Strip the dot if (!fileExtension) { return; } - await this.extensionService.whenInstalledExtensionsRegistered(); const mimeTypes = guessMimeTypes(uri); if (mimeTypes.length !== 1 || mimeTypes[0] !== MIME_UNKNOWN) { return; @@ -209,7 +240,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { this.promptRecommendedExtensionForFileExtension(fileExtension, installed); } - private async promptRecommendedExtensionForFileType(recommendations: string[], installed: IExtension[]): Promise { + private async promptRecommendedExtensionForFileType(name: string, recommendations: string[], installed: IExtension[]): Promise { recommendations = this.filterIgnoredOrNotAllowed(recommendations); if (recommendations.length === 0) { @@ -222,17 +253,12 @@ export class FileBasedRecommendations extends ExtensionRecommendations { } const extensionId = recommendations[0]; - const entry = this.importantExtensionTips[extensionId]; + const entry = this.importantExtensionTips.get(extensionId); if (!entry) { return false; } - const extensionName = entry.name; - let message = localize('reallyRecommended2', "The '{0}' extension is recommended for this file type.", extensionName); - if (entry.isExtensionPack) { - message = localize('reallyRecommendedExtensionPack', "The '{0}' extension pack is recommended for this file type.", extensionName); - } - this.promptImportantExtensionsInstallNotification([extensionId], message); + this.promptImportantExtensionsInstallNotification([extensionId], localize('reallyRecommended', "Do you want to install the recommended extensions for {0}?", name), `@id:${extensionId}`); return true; } @@ -300,7 +326,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { private getCachedRecommendations(): IStringDictionary { let storedRecommendations = JSON.parse(this.storageService.get(recommendationsStorageKey, StorageScope.GLOBAL, '[]')); - if (Array.isArray(storedRecommendations)) { + if (Array.isArray(storedRecommendations)) { storedRecommendations = storedRecommendations.reduce((result, id) => { result[id] = Date.now(); return result; }, >{}); } const result: IStringDictionary = {}; @@ -314,7 +340,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { private storeCachedRecommendations(): void { const storedRecommendations: IStringDictionary = {}; - forEach(this.fileBasedRecommendations, ({ key, value }) => storedRecommendations[key] = value.recommendedTime); + this.fileBasedRecommendations.forEach((value, key) => storedRecommendations[key] = value.recommendedTime); this.storageService.store(recommendationsStorageKey, JSON.stringify(storedRecommendations), StorageScope.GLOBAL); } } diff --git a/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts index 4bfefe4941c..517086adfa5 100644 --- a/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts @@ -12,6 +12,8 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; export class KeymapRecommendations extends ExtensionRecommendations { @@ -26,9 +28,11 @@ export class KeymapRecommendations extends ExtensionRecommendations { @INotificationService notificationService: INotificationService, @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, extensionsWorkbenchService, extensionManagementService, storageKeysSyncRegistryService); } protected async doActivate(): Promise { diff --git a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts index 1175c59a129..ac8603ff2f8 100644 --- a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts @@ -10,31 +10,26 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { distinct, flatten, coalesce } from 'vs/base/common/arrays'; import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { IExtensionsConfigContent, ExtensionRecommendationSource, ExtensionRecommendationReason, IWorkbenchExtensionEnablementService, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IExtensionsConfigContent, ExtensionRecommendationSource, ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { parse } from 'vs/base/common/json'; -import { EXTENSIONS_CONFIG } from 'vs/workbench/contrib/extensions/common/extensions'; +import { EXTENSIONS_CONFIG, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; import { localize } from 'vs/nls'; -import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ShowRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; -import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage'; +import { IStorageService } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; - -type ExtensionWorkspaceRecommendationsNotificationClassification = { - userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; -}; - -const choiceNever = localize('neverShowAgain', "Don't Show Again"); -const ignoreWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore'; +import { Emitter } from 'vs/base/common/event'; export class WorkspaceRecommendations extends ExtensionRecommendations { private _recommendations: ExtensionRecommendation[] = []; get recommendations(): ReadonlyArray { return this._recommendations; } + private _onDidChangeRecommendations = this._register(new Emitter()); + readonly onDidChangeRecommendations = this._onDidChangeRecommendations.event; + private _ignoredRecommendations: string[] = []; get ignoredRecommendations(): ReadonlyArray { return this._ignoredRecommendations; } @@ -44,22 +39,21 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @ILogService private readonly logService: ILogService, @IFileService private readonly fileService: IFileService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @INotificationService notificationService: INotificationService, @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, extensionsWorkbenchService, extensionManagementService, storageKeysSyncRegistryService); } protected async doActivate(): Promise { await this.fetch(); this._register(this.contextService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e))); - this.promptWorkspaceRecommendations(); } /** @@ -71,7 +65,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { const { invalidRecommendations, message } = await this.validateExtensions(extensionsConfigBySource.map(({ contents }) => contents)); if (invalidRecommendations.length) { - this.notificationService.warn(`The below ${invalidRecommendations.length} extension(s) in workspace recommendations have issues:\n${message}`); + this.notificationService.warn(`The ${invalidRecommendations.length} extension(s) below, in workspace recommendations have issues:\n${message}`); } this._ignoredRecommendations = []; @@ -97,63 +91,6 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { } } - private async promptWorkspaceRecommendations(): Promise { - const allowedRecommendations = this.recommendations.filter(rec => this.isExtensionAllowedToBeRecommended(rec.extensionId)); - - if (allowedRecommendations.length === 0 || this.hasToIgnoreWorkspaceRecommendationNotifications()) { - return; - } - - let installed = await this.extensionManagementService.getInstalled(); - installed = installed.filter(l => this.extensionEnablementService.getEnablementState(l) !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind - const recommendations = allowedRecommendations.filter(({ extensionId }) => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier))); - - if (!recommendations.length) { - return; - } - - return new Promise(c => { - this.notificationService.prompt( - Severity.Info, - localize('workspaceRecommended', "This workspace has extension recommendations."), - [{ - label: localize('installAll', "Install All"), - run: () => { - this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'install' }); - const installAllAction = this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, recommendations.map(({ extensionId }) => extensionId)); - installAllAction.run(); - installAllAction.dispose(); - c(undefined); - } - }, { - label: localize('showRecommendations', "Show Recommendations"), - run: () => { - this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'show' }); - const showAction = this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, localize('showRecommendations', "Show Recommendations")); - showAction.run(); - showAction.dispose(); - c(undefined); - } - }, { - label: choiceNever, - isSecondary: true, - run: () => { - this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'neverShowAgain' }); - this.storageService.store(ignoreWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE); - c(undefined); - } - }], - { - sticky: true, - onCancel: () => { - this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'cancelled' }); - c(undefined); - } - } - ); - }); - } - private async fetchExtensionsConfigBySource(): Promise<{ contents: IExtensionsConfigContent, source: ExtensionRecommendationSource }[]> { const workspace = this.contextService.getWorkspace(); const result = await Promise.all([ @@ -235,7 +172,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { await this.fetch(); // Suggest only if at least one of the newly added recommendations was not suggested before if (this._recommendations.some(current => oldWorkspaceRecommended.every(old => current.extensionId !== old.extensionId))) { - this.promptWorkspaceRecommendations(); + this._onDidChangeRecommendations.fire(); } } } @@ -250,8 +187,5 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { return null; } - private hasToIgnoreWorkspaceRecommendationNotifications(): boolean { - return this.hasToIgnoreRecommendationNotifications() || this.storageService.getBoolean(ignoreWorkspaceRecommendationsStorageKey, StorageScope.WORKSPACE, false); - } } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts index 6d0222c798e..4a098ffd88f 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts @@ -24,8 +24,11 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; import { OpenExtensionsFolderAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; import { ExtensionsLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; // Singletons +registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); // TODO@sandbox TODO@ben move back into common/extensions.contribution.ts when 'semver-umd' can be loaded registerSingleton(IExtensionHostProfileService, ExtensionHostProfileService, true); const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts index 1c780f7fb4f..9715cea96ce 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import * as os from 'os'; import { IProductService } from 'vs/platform/product/common/productService'; import { Action, IAction, Separator } from 'vs/base/common/actions'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions'; @@ -100,7 +100,7 @@ interface IRuntimeExtension { unresponsiveProfile?: IExtensionHostProfile; } -export class RuntimeExtensionsEditor extends BaseEditor { +export class RuntimeExtensionsEditor extends EditorPane { public static readonly ID: string = 'workbench.editor.runtimeExtensions'; @@ -233,11 +233,24 @@ export class RuntimeExtensionsEditor extends BaseEditor { result = result.filter(element => element.status.activationTimes); // bubble up extensions that have caused slowness + + const isUnresponsive = (extension: IRuntimeExtension): boolean => + extension.unresponsiveProfile === this._profileInfo; + + const profileTime = (extension: IRuntimeExtension): number => + extension.profileInfo?.totalTime ?? 0; + + const activationTime = (extension: IRuntimeExtension): number => + (extension.status.activationTimes?.codeLoadingTime ?? 0) + + (extension.status.activationTimes?.activateCallTime ?? 0); + result = result.sort((a, b) => { - if (a.unresponsiveProfile === this._profileInfo && !b.unresponsiveProfile) { - return -1; - } else if (!a.unresponsiveProfile && b.unresponsiveProfile === this._profileInfo) { - return 1; + if (isUnresponsive(a) || isUnresponsive(b)) { + return +isUnresponsive(b) - +isUnresponsive(a); + } else if (profileTime(a) || profileTime(b)) { + return profileTime(b) - profileTime(a); + } else if (activationTime(a) || activationTime(b)) { + return activationTime(b) - activationTime(a); } return a.originalIndex - b.originalIndex; }); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts index b20db7d9a94..0ac3774a174 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts @@ -33,8 +33,7 @@ import { IPager } from 'vs/base/common/paging'; import { assign } from 'vs/base/common/objects'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; -import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { ConfigurationKey, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test'; import { IURLService } from 'vs/platform/url/common/url'; import { ITextModel } from 'vs/editor/common/model'; @@ -58,6 +57,8 @@ import { ExtensionRecommendationsService } from 'vs/workbench/contrib/extensions import { NoOpWorkspaceTagsService } from 'vs/workbench/contrib/tags/browser/workspaceTagsService'; import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspaceTags'; import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; const mockExtensionGallery: IGalleryExtension[] = [ aGalleryExtension('MockExtension1', { @@ -199,11 +200,18 @@ suite('ExtensionRecommendationsService Test', () => { testConfigurationService = new TestConfigurationService(); instantiationService.stub(IConfigurationService, testConfigurationService); instantiationService.stub(INotificationService, new TestNotificationService()); - instantiationService.stub(IExtensionManagementService, ExtensionManagementService); - instantiationService.stub(IExtensionManagementService, 'onInstallExtension', installEvent.event); - instantiationService.stub(IExtensionManagementService, 'onDidInstallExtension', didInstallEvent.event); - instantiationService.stub(IExtensionManagementService, 'onUninstallExtension', uninstallEvent.event); - instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event); + instantiationService.stub(IExtensionManagementService, >{ + onInstallExtension: installEvent.event, + onDidInstallExtension: didInstallEvent.event, + onUninstallExtension: uninstallEvent.event, + onDidUninstallExtension: didUninstallEvent.event, + async getInstalled() { return []; }, + async canInstall() { return true; }, + async getExtensionsReport() { return []; }, + }); + instantiationService.stub(IExtensionService, >{ + async whenInstalledExtensionsRegistered() { return true; } + }); instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IURLService, NativeURLService); @@ -231,6 +239,7 @@ suite('ExtensionRecommendationsService Test', () => { experimentService = instantiationService.createInstance(TestExperimentService); instantiationService.stub(IExperimentService, experimentService); + instantiationService.set(IExtensionsWorkbenchService, instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.stub(IExtensionTipsService, instantiationService.createInstance(ExtensionTipsService)); onModelAddedEvent = new Emitter(); @@ -302,7 +311,7 @@ suite('ExtensionRecommendationsService Test', () => { function testNoPromptForValidRecommendations(recommendations: string[]) { return setUpFolderWorkspace('myFolder', recommendations).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { assert.equal(Object.keys(testObject.getAllRecommendationsWithReason()).length, recommendations.length); assert.ok(!prompted); }); @@ -338,20 +347,18 @@ suite('ExtensionRecommendationsService Test', () => { return testNoPromptForValidRecommendations([]); }); - test('ExtensionRecommendationsService: Prompt for valid workspace recommendations', () => { - return setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { - const recommendations = Object.keys(testObject.getAllRecommendationsWithReason()); + test('ExtensionRecommendationsService: Prompt for valid workspace recommendations', async () => { + await setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions); + testObject = instantiationService.createInstance(ExtensionRecommendationsService); + await testObject.activationPromise; - assert.equal(recommendations.length, mockTestData.validRecommendedExtensions.length); - mockTestData.validRecommendedExtensions.forEach(x => { - assert.equal(recommendations.indexOf(x.toLowerCase()) > -1, true); - }); - - assert.ok(prompted); - }); + const recommendations = Object.keys(testObject.getAllRecommendationsWithReason()); + assert.equal(recommendations.length, mockTestData.validRecommendedExtensions.length); + mockTestData.validRecommendedExtensions.forEach(x => { + assert.equal(recommendations.indexOf(x.toLowerCase()) > -1, true); }); + + assert.ok(prompted); }); test('ExtensionRecommendationsService: No Prompt for valid workspace recommendations if they are already installed', () => { @@ -373,7 +380,7 @@ suite('ExtensionRecommendationsService Test', () => { testConfigurationService.setUserConfiguration(ConfigurationKey, { showRecommendationsOnlyOnDemand: true }); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { assert.ok(!prompted); }); }); @@ -391,7 +398,7 @@ suite('ExtensionRecommendationsService Test', () => { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been globally ignored assert.ok(recommendations['ms-python.python']); // stored recommendation @@ -409,7 +416,7 @@ suite('ExtensionRecommendationsService Test', () => { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, ignoredRecommendations).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been workspace ignored assert.ok(recommendations['ms-python.python']); // stored recommendation @@ -430,7 +437,7 @@ suite('ExtensionRecommendationsService Test', () => { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, workspaceIgnoredRecommendations).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(recommendations['ms-python.python']); @@ -449,7 +456,7 @@ suite('ExtensionRecommendationsService Test', () => { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(recommendations['ms-python.python']); assert.ok(recommendations['mockpublisher1.mockextension1']); @@ -486,7 +493,7 @@ suite('ExtensionRecommendationsService Test', () => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); testObject.onRecommendationChange(changeHandlerTarget); testObject.toggleIgnoredRecommendation(ignoredExtensionId, true); - await testObject.loadWorkspaceConfigPromise; + await testObject.activationPromise; assert.ok(changeHandlerTarget.calledOnce); assert.ok(changeHandlerTarget.getCall(0).calledWithMatch({ extensionId: ignoredExtensionId.toLowerCase(), isRecommended: false })); @@ -498,7 +505,7 @@ suite('ExtensionRecommendationsService Test', () => { return setUpFolderWorkspace('myFolder', []).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { const recommendations = testObject.getFileBasedRecommendations(); assert.equal(recommendations.length, 2); assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-dotnettools.csharp')); // stored recommendation that exists in product.extensionTips @@ -517,7 +524,7 @@ suite('ExtensionRecommendationsService Test', () => { return setUpFolderWorkspace('myFolder', []).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { const recommendations = testObject.getFileBasedRecommendations(); assert.equal(recommendations.length, 2); assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-dotnettools.csharp')); // stored recommendation that exists in product.extensionTips diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts index 7c83b8ea7b1..e2f71dede95 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts @@ -101,7 +101,7 @@ async function setupTest() { instantiationService.stub(IExtensionManagementServerService, new class extends ExtensionManagementServerService { #localExtensionManagementServer: IExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', id: 'vscode-local' }; constructor() { - super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IConfigurationService), instantiationService.get(IProductService), instantiationService.get(ILogService), instantiationService.get(ILabelService)); + super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(ILabelService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IProductService), instantiationService.get(IConfigurationService), instantiationService.get(ILogService)); } get localExtensionManagementServer(): IExtensionManagementServer { return this.#localExtensionManagementServer; } set localExtensionManagementServer(server: IExtensionManagementServer) { } diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts index cbbaddea537..e2a88db3452 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts @@ -16,7 +16,6 @@ import { } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IExtensionRecommendationsService, ExtensionRecommendationReason, IExtensionRecommendation } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IURLService } from 'vs/platform/url/common/url'; @@ -40,13 +39,13 @@ import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browse import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { ExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService'; -import { IProductService } from 'vs/platform/product/common/productService'; import { ILabelService } from 'vs/platform/label/common/label'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { IMenuService } from 'vs/platform/actions/common/actions'; import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; +import { IProductService } from 'vs/platform/product/common/productService'; suite('ExtensionsListView Tests', () => { @@ -68,6 +67,7 @@ suite('ExtensionsListView Tests', () => { const workspaceRecommendationA = aGalleryExtension('workspace-recommendation-A'); const workspaceRecommendationB = aGalleryExtension('workspace-recommendation-B'); const configBasedRecommendationA = aGalleryExtension('configbased-recommendation-A'); + const configBasedRecommendationB = aGalleryExtension('configbased-recommendation-B'); const fileBasedRecommendationA = aGalleryExtension('filebased-recommendation-A'); const fileBasedRecommendationB = aGalleryExtension('filebased-recommendation-B'); const otherRecommendationA = aGalleryExtension('other-recommendation-A'); @@ -89,11 +89,15 @@ suite('ExtensionsListView Tests', () => { instantiationService.stub(ISharedProcessService, TestSharedProcessService); instantiationService.stub(IExperimentService, ExperimentService); - instantiationService.stub(IExtensionManagementService, ExtensionManagementService); - instantiationService.stub(IExtensionManagementService, 'onInstallExtension', installEvent.event); - instantiationService.stub(IExtensionManagementService, 'onDidInstallExtension', didInstallEvent.event); - instantiationService.stub(IExtensionManagementService, 'onUninstallExtension', uninstallEvent.event); - instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event); + instantiationService.stub(IExtensionManagementService, >{ + onInstallExtension: installEvent.event, + onDidInstallExtension: didInstallEvent.event, + onUninstallExtension: uninstallEvent.event, + onDidUninstallExtension: didUninstallEvent.event, + async getInstalled() { return []; }, + async canInstall() { return true; }, + async getExtensionsReport() { return []; }, + }); instantiationService.stub(IRemoteAgentService, RemoteAgentService); instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IMenuService, new TestMenuService()); @@ -101,7 +105,7 @@ suite('ExtensionsListView Tests', () => { instantiationService.stub(IExtensionManagementServerService, new class extends ExtensionManagementServerService { #localExtensionManagementServer: IExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', id: 'vscode-local' }; constructor() { - super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IConfigurationService), instantiationService.get(IProductService), instantiationService.get(ILogService), instantiationService.get(ILabelService)); + super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(ILabelService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IProductService), instantiationService.get(IConfigurationService), instantiationService.get(ILogService)); } get localExtensionManagementServer(): IExtensionManagementServer { return this.#localExtensionManagementServer; } set localExtensionManagementServer(server: IExtensionManagementServer) { } @@ -123,9 +127,10 @@ suite('ExtensionsListView Tests', () => { { extensionId: workspaceRecommendationB.identifier.id }]); }, getConfigBasedRecommendations() { - return Promise.resolve([ - { extensionId: configBasedRecommendationA.identifier.id } - ]); + return Promise.resolve({ + important: [{ extensionId: configBasedRecommendationA.identifier.id }], + others: [{ extensionId: configBasedRecommendationB.identifier.id }], + }); }, getImportantRecommendations(): Promise { return Promise.resolve([]); @@ -138,6 +143,7 @@ suite('ExtensionsListView Tests', () => { }, getOtherRecommendations() { return Promise.resolve([ + { extensionId: configBasedRecommendationB.identifier.id }, { extensionId: otherRecommendationA.identifier.id } ]); }, @@ -333,7 +339,8 @@ suite('ExtensionsListView Tests', () => { test('Test @recommended:workspace query', () => { const workspaceRecommendedExtensions = [ workspaceRecommendationA, - workspaceRecommendationB + workspaceRecommendationB, + configBasedRecommendationA, ]; const target = instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...workspaceRecommendedExtensions)); @@ -351,9 +358,9 @@ suite('ExtensionsListView Tests', () => { test('Test @recommended query', () => { const allRecommendedExtensions = [ - configBasedRecommendationA, fileBasedRecommendationA, fileBasedRecommendationB, + configBasedRecommendationB, otherRecommendationA ]; const target = instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...allRecommendedExtensions)); @@ -379,7 +386,8 @@ suite('ExtensionsListView Tests', () => { configBasedRecommendationA, fileBasedRecommendationA, fileBasedRecommendationB, - otherRecommendationA + configBasedRecommendationB, + otherRecommendationA, ]; const target = instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...allRecommendedExtensions)); diff --git a/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts b/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts index 067cc377d6c..42dba3506e5 100644 --- a/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts +++ b/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts @@ -306,7 +306,6 @@ export class LinuxExternalTerminalService implements IExternalTerminalService { LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY = new Promise(async r => { if (env.isLinux) { const isDebian = await pfs.exists('/etc/debian_version'); - await process.lazyEnv; if (isDebian) { r('x-terminal-emulator'); } else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') { diff --git a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts index 1daf1517449..7b93fdb20c7 100644 --- a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts @@ -49,13 +49,13 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { } private async openInternal(input: EditorInput, options: EditorOptions | undefined): Promise { - if (input instanceof FileEditorInput) { + if (input instanceof FileEditorInput && this.group) { + + // Enforce to open the input as text to enable our text based viewer input.setForceOpenAsText(); - if (this.group !== undefined) { - await openEditorWith(input, undefined, options, this.group, this.editorService, this.configurationService, this.quickInputService); - } else { - await this.editorService.openEditor(input, options, this.group); - } + + // If more editors are installed that can handle this input, show a picker + await openEditorWith(input, undefined, options, this.group, this.editorService, this.configurationService, this.quickInputService); } } diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index 4beec737735..d40c20d8c66 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -12,7 +12,7 @@ import { Action } from 'vs/base/common/actions'; import { VIEWLET_ID, TEXT_FILE_EDITOR_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files'; import { ITextFileService, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; -import { EditorOptions, TextEditorOptions, IEditorInput } from 'vs/workbench/common/editor'; +import { EditorOptions, TextEditorOptions, IEditorInput, IEditorOpenContext } from 'vs/workbench/common/editor'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -91,13 +91,13 @@ export class TextFileEditor extends BaseTextEditor { return this._input as FileEditorInput; } - async setInput(input: FileEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: FileEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { // Update/clear view settings if input changes this.doSaveOrClearTextEditorViewState(this.input); // Set input and resolve - await super.setInput(input, options, token); + await super.setInput(input, options, context, token); try { const resolvedModel = await input.resolve(); @@ -119,10 +119,12 @@ export class TextFileEditor extends BaseTextEditor { const textEditor = assertIsDefined(this.getControl()); textEditor.setModel(textFileModel.textEditorModel); - // Always restore View State if any associated - const editorViewState = this.loadTextEditorViewState(input.resource); - if (editorViewState) { - textEditor.restoreViewState(editorViewState); + // Always restore View State if any associated and not disabled via settings + if (this.shouldRestoreTextEditorViewState(input, context)) { + const editorViewState = this.loadTextEditorViewState(input.resource); + if (editorViewState) { + textEditor.restoreViewState(editorViewState); + } } // TextOptions (avoiding instanceof here for a reason, do not change!) @@ -242,7 +244,7 @@ export class TextFileEditor extends BaseTextEditor { // If the user configured to not restore view state, we clear the view // state unless the editor is still opened in the group. - if (!this.shouldRestoreViewState && (!this.group || !this.group.isOpened(input))) { + if (!this.shouldRestoreTextEditorViewState(input) && (!this.group || !this.group.isOpened(input))) { this.clearTextEditorViewState([input.resource], this.group); } diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index 441537060af..fa238a5bf0f 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -11,7 +11,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { ExplorerFocusCondition, TextFileContentProvider, VIEWLET_ID, IExplorerService, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, FilesExplorerFocusCondition } from 'vs/workbench/contrib/files/common/files'; +import { ExplorerFocusCondition, TextFileContentProvider, VIEWLET_ID, IExplorerService, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, FilesExplorerFocusCondition, ExplorerFolderContext } from 'vs/workbench/contrib/files/common/files'; import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -144,6 +144,24 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + weight: KeybindingWeight.WorkbenchContrib + 10, + when: ContextKeyExpr.and(ExplorerFocusCondition, ExplorerFolderContext.toNegated()), + primary: KeyCode.Enter, + mac: { + primary: KeyMod.CtrlCmd | KeyCode.DownArrow + }, + id: 'explorer.openAndPassFocus', handler: async (accessor, _resource: URI | object) => { + const editorService = accessor.get(IEditorService); + const explorerService = accessor.get(IExplorerService); + const resources = explorerService.getContext(true); + + if (resources.length) { + await editorService.openEditors(resources.map(r => ({ resource: r.resource, options: { preserveFocus: false } }))); + } + } +}); + const COMPARE_WITH_SAVED_SCHEMA = 'showModifications'; let providerDisposables: IDisposable[] = []; KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -637,3 +655,5 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } } }); + + diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 71f86a19285..7be3b5c9b81 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -361,10 +361,23 @@ configurationRegistry.registerConfiguration({ properties: { 'editor.formatOnSave': { 'type': 'boolean', - 'default': false, 'description': nls.localize('formatOnSave', "Format a file on save. A formatter must be available, the file must not be saved after delay, and the editor must not be shutting down."), - scope: ConfigurationScope.LANGUAGE_OVERRIDABLE, - } + 'scope': ConfigurationScope.LANGUAGE_OVERRIDABLE, + }, + 'editor.formatOnSaveMode': { + 'type': 'string', + 'default': 'file', + 'enum': [ + 'file', + 'modifications' + ], + 'enumDescriptions': [ + nls.localize('everything', "Format the whole file."), + nls.localize('modification', "Format modifications (requires source control)."), + ], + 'markdownDescription': nls.localize('formatOnSaveMode', "Controls if format on save formats the whole file or only modifications. Only applies when `#editor.formatOnSave#` is `true`."), + 'scope': ConfigurationScope.LANGUAGE_OVERRIDABLE, + }, } }); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 66d94162794..c35e3fa820b 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -66,7 +66,7 @@ function hasExpandedRootChild(tree: WorkbenchCompressibleAsyncDataTree { } if (statA.isDirectory && statB.isDirectory) { - return compareFileNamesNumeric(statA.name, statB.name); + return compareFileNamesDefault(statA.name, statB.name); } break; @@ -699,17 +699,17 @@ export class FileSorter implements ITreeSorter { // Sort Files switch (sortOrder) { case 'type': - return compareFileExtensionsNumeric(statA.name, statB.name); + return compareFileExtensionsDefault(statA.name, statB.name); case 'modified': if (statA.mtime !== statB.mtime) { return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1; } - return compareFileNamesNumeric(statA.name, statB.name); + return compareFileNamesDefault(statA.name, statB.name); default: /* 'default', 'mixed', 'filesFirst' */ - return compareFileNamesNumeric(statA.name, statB.name); + return compareFileNamesDefault(statA.name, statB.name); } } } @@ -839,11 +839,11 @@ export class FileDragAndDrop implements ITreeDragAndDrop { private handleDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction { const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh)); - const fromDesktop = data instanceof DesktopDragAndDropData; - const effect = (fromDesktop || isCopy) ? ListDragOverEffect.Copy : ListDragOverEffect.Move; + const isNative = data instanceof NativeDragAndDropData; + const effect = (isNative || isCopy) ? ListDragOverEffect.Copy : ListDragOverEffect.Move; - // Desktop DND - if (fromDesktop) { + // Native DND + if (isNative) { if (!containsDragType(originalEvent, DataTransfers.FILES, CodeDataTransfers.FILES)) { return false; } @@ -979,7 +979,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } // Desktop DND (Import file) - if (data instanceof DesktopDragAndDropData) { + if (data instanceof NativeDragAndDropData) { if (isWeb) { this.handleWebExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e)); } else { @@ -992,7 +992,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } } - private async handleWebExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { + private async handleWebExternalDrop(data: NativeDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { const items = (originalEvent.dataTransfer as unknown as IWebkitDataTransfer).items; // Somehow the items thing is being modified at random, maybe as a security @@ -1205,7 +1205,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { }); } - private async handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { + private async handleExternalDrop(data: NativeDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { // Check for dropped external files to be folders const droppedResources = extractResources(originalEvent, true); diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 843ea8cd16f..e07fd997901 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -38,7 +38,7 @@ import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; import { memoize } from 'vs/base/common/decorators'; -import { ElementsDragAndDropData, DesktopDragAndDropData } from 'vs/base/browser/ui/list/listView'; +import { ElementsDragAndDropData, NativeDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { URI } from 'vs/base/common/uri'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { isWeb } from 'vs/base/common/platform'; @@ -667,7 +667,7 @@ class OpenEditorsDragAndDrop implements IListDragAndDrop { + async provideTextContent(resource: URI): Promise { + if (!resource.query) { + // We require the URI to use the `query` to transport the original scheme and query + // as done by `resourceToTextFile` + return null; + } + const savedFileResource = TextFileContentProvider.textFileToResource(resource); // Make sure our text file is resolved up to date diff --git a/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts b/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts index 6b29c6e66d9..70c2fa9e2d4 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts @@ -26,7 +26,9 @@ suite('Files - FileOnDiskContentProvider', () => { const content = await provider.provideTextContent(uri.with({ scheme: 'conflictResolution', query: JSON.stringify({ scheme: uri.scheme }) })); - assert.equal(snapshotToString(content.createSnapshot()), 'Hello Html'); - assert.equal(accessor.fileService.getLastReadFileUri().toString(), uri.toString()); + assert.ok(content); + assert.equal(snapshotToString(content!.createSnapshot()), 'Hello Html'); + assert.equal(accessor.fileService.getLastReadFileUri().scheme, uri.scheme); + assert.equal(accessor.fileService.getLastReadFileUri().path, uri.path); }); }); diff --git a/src/vs/workbench/contrib/format/browser/format.contribution.ts b/src/vs/workbench/contrib/format/browser/format.contribution.ts index 56a34671c9e..a91827cdc47 100644 --- a/src/vs/workbench/contrib/format/browser/format.contribution.ts +++ b/src/vs/workbench/contrib/format/browser/format.contribution.ts @@ -5,3 +5,4 @@ import './formatActionsMultiple'; import './formatActionsNone'; +import './formatModified'; diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index 26599aef2be..eb9e20df494 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts @@ -12,7 +12,7 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { formatDocumentRangeWithProvider, formatDocumentWithProvider, getRealAndSyntheticDocumentFormattersOrdered, FormattingConflicts, FormattingMode } from 'vs/editor/contrib/format/format'; +import { formatDocumentRangesWithProvider, formatDocumentWithProvider, getRealAndSyntheticDocumentFormattersOrdered, FormattingConflicts, FormattingMode } from 'vs/editor/contrib/format/format'; import { Range } from 'vs/editor/common/core/range'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @@ -308,7 +308,7 @@ registerEditorAction(class FormatSelectionMultipleAction extends EditorAction { const provider = DocumentRangeFormattingEditProviderRegistry.ordered(model); const pick = await instaService.invokeFunction(showFormatterPick, model, provider); if (typeof pick === 'number') { - await instaService.invokeFunction(formatDocumentRangeWithProvider, provider[pick], editor, range, CancellationToken.None); + await instaService.invokeFunction(formatDocumentRangesWithProvider, provider[pick], editor, range, CancellationToken.None); } logFormatterTelemetry(telemetryService, 'range', provider, typeof pick === 'number' && provider[pick] || undefined); diff --git a/src/vs/workbench/contrib/format/browser/formatActionsNone.ts b/src/vs/workbench/contrib/format/browser/formatActionsNone.ts index 18b3e2b6f8d..9355b4e6dc1 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsNone.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsNone.ts @@ -14,7 +14,14 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { ICommandService } from 'vs/platform/commands/common/commands'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { showExtensionQuery } from 'vs/workbench/contrib/format/browser/showExtensionQuery'; +import { VIEWLET_ID, IExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/common/extensions'; + +async function showExtensionQuery(viewletService: IViewletService, query: string) { + const viewlet = await viewletService.openViewlet(VIEWLET_ID, true); + if (viewlet) { + (viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer).search(query); + } +} registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { diff --git a/src/vs/workbench/contrib/format/browser/formatModified.ts b/src/vs/workbench/contrib/format/browser/formatModified.ts new file mode 100644 index 00000000000..e1ae53814d7 --- /dev/null +++ b/src/vs/workbench/contrib/format/browser/formatModified.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CancellationToken } from 'vs/base/common/cancellation'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { Range } from 'vs/editor/common/core/range'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { ITextModel } from 'vs/editor/common/model'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { formatDocumentRangesWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; +import * as nls from 'vs/nls'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { Progress } from 'vs/platform/progress/common/progress'; +import { getOriginalResource } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator'; +import { ISCMService } from 'vs/workbench/contrib/scm/common/scm'; + +registerEditorAction(class FormatModifiedAction extends EditorAction { + + constructor() { + super({ + id: 'editor.action.formatChanges', + label: nls.localize('formatChanges', "Format Modified Lines"), + alias: 'Format Modified Lines', + precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasDocumentSelectionFormattingProvider), + }); + } + + async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + const instaService = accessor.get(IInstantiationService); + + if (!editor.hasModel()) { + return; + } + + const ranges = await instaService.invokeFunction(getModifiedRanges, editor.getModel()); + if (isNonEmptyArray(ranges)) { + return instaService.invokeFunction( + formatDocumentRangesWithSelectedProvider, editor, ranges, + FormattingMode.Explicit, Progress.None, CancellationToken.None + ); + } + } +}); + + +export async function getModifiedRanges(accessor: ServicesAccessor, modified: ITextModel): Promise { + const scmService = accessor.get(ISCMService); + const workerService = accessor.get(IEditorWorkerService); + const modelService = accessor.get(ITextModelService); + + const original = await getOriginalResource(scmService, modified.uri); + if (!original) { + return undefined; + } + + const ranges: Range[] = []; + const ref = await modelService.createModelReference(original); + try { + if (!workerService.canComputeDirtyDiff(original, modified.uri)) { + return undefined; + } + const changes = await workerService.computeDirtyDiff(original, modified.uri, true); + if (!isNonEmptyArray(changes)) { + return undefined; + } + for (let change of changes) { + ranges.push(modified.validateRange(new Range( + change.modifiedStartLineNumber, 1, + change.modifiedEndLineNumber || change.modifiedStartLineNumber /*endLineNumber is 0 when things got deleted*/, Number.MAX_SAFE_INTEGER) + )); + } + } finally { + ref.dispose(); + } + + return ranges; +} diff --git a/src/vs/workbench/contrib/format/browser/showExtensionQuery.ts b/src/vs/workbench/contrib/format/browser/showExtensionQuery.ts deleted file mode 100644 index cfb1a4da14d..00000000000 --- a/src/vs/workbench/contrib/format/browser/showExtensionQuery.ts +++ /dev/null @@ -1,15 +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 { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { VIEWLET_ID, IExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/common/extensions'; - -export function showExtensionQuery(viewletService: IViewletService, query: string) { - return viewletService.openViewlet(VIEWLET_ID, true).then(viewlet => { - if (viewlet) { - (viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer).search(query); - } - }); -} diff --git a/src/vs/workbench/contrib/markers/browser/constants.ts b/src/vs/workbench/contrib/markers/browser/constants.ts index 6714a443377..963ec0c8130 100644 --- a/src/vs/workbench/contrib/markers/browser/constants.ts +++ b/src/vs/workbench/contrib/markers/browser/constants.ts @@ -17,6 +17,7 @@ export default { MARKERS_VIEW_CLEAR_FILTER_TEXT: 'problems.action.clearFilterText', MARKERS_VIEW_SHOW_MULTILINE_MESSAGE: 'problems.action.showMultilineMessage', MARKERS_VIEW_SHOW_SINGLELINE_MESSAGE: 'problems.action.showSinglelineMessage', + MARKER_OPEN_ACTION_ID: 'problems.action.open', MARKER_OPEN_SIDE_ACTION_ID: 'problems.action.openToSide', MARKER_SHOW_PANEL_ID: 'workbench.action.showErrorsWarnings', MARKER_SHOW_QUICK_FIX: 'problems.action.showQuickFixes', diff --git a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts index 040dcdfcb94..5ded9c8459f 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts @@ -36,6 +36,21 @@ import { Codicon } from 'vs/base/common/codicons'; registerSingleton(IMarkersWorkbenchService, MarkersWorkbenchService, false); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: Constants.MARKER_OPEN_ACTION_ID, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.MarkerFocusContextKey), + primary: KeyCode.Enter, + mac: { + primary: KeyCode.Enter, + secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow] + }, + handler: (accessor, args: any) => { + const markersView = accessor.get(IViewsService).getActiveViewWithId(Constants.MARKERS_VIEW_ID)!; + markersView.openFileAtElement(markersView.getFocusElement(), false, false, true); + } +}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.MARKER_OPEN_SIDE_ACTION_ID, weight: KeybindingWeight.WorkbenchContrib, diff --git a/src/vs/workbench/contrib/markers/browser/markersView.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts index c7cff71ecf5..528a7beeffc 100644 --- a/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -333,7 +333,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { private setTreeSelection(): void { if (this.tree && this.tree.getSelection().length === 0) { - const firstMarker = this.markersWorkbenchService.markersModel.resourceMarkers[0].markers[0]; + const firstMarker = this.markersWorkbenchService.markersModel.resourceMarkers[0]?.markers[0]; if (firstMarker) { this.tree.setFocus([firstMarker]); this.tree.setSelection([firstMarker]); @@ -692,18 +692,20 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { if (typeof autoReveal === 'boolean' && autoReveal) { let currentActiveResource = this.getResourceForCurrentActiveResource(); if (currentActiveResource) { - if (this.tree.hasElement(currentActiveResource) && !this.tree.isCollapsed(currentActiveResource) && this.hasSelectedMarkerFor(currentActiveResource)) { - this.tree.reveal(this.tree.getSelection()[0], this.lastSelectedRelativeTop); - if (focus) { - this.tree.setFocus(this.tree.getSelection()); - } - } else { - this.tree.expand(currentActiveResource); - this.tree.reveal(currentActiveResource, 0); + if (this.tree.hasElement(currentActiveResource)) { + if (!this.tree.isCollapsed(currentActiveResource) && this.hasSelectedMarkerFor(currentActiveResource)) { + this.tree.reveal(this.tree.getSelection()[0], this.lastSelectedRelativeTop); + if (focus) { + this.tree.setFocus(this.tree.getSelection()); + } + } else { + this.tree.expand(currentActiveResource); + this.tree.reveal(currentActiveResource, 0); - if (focus) { - this.tree.setFocus([currentActiveResource]); - this.tree.setSelection([currentActiveResource]); + if (focus) { + this.tree.setFocus([currentActiveResource]); + this.tree.setSelection([currentActiveResource]); + } } } } else if (focus) { diff --git a/src/vs/workbench/contrib/markers/browser/media/markers.css b/src/vs/workbench/contrib/markers/browser/media/markers.css index b898358d421..ec967357971 100644 --- a/src/vs/workbench/contrib/markers/browser/media/markers.css +++ b/src/vs/workbench/contrib/markers/browser/media/markers.css @@ -65,7 +65,7 @@ } .panel > .title .monaco-action-bar .action-item.markers-panel-action-filter-container { - max-width: 600px; + max-width: 400px; min-width: 300px; margin-right: 10px; } diff --git a/src/vs/workbench/contrib/markers/browser/messages.ts b/src/vs/workbench/contrib/markers/browser/messages.ts index 4fbffb9d7d4..4652f48e7f1 100644 --- a/src/vs/workbench/contrib/markers/browser/messages.ts +++ b/src/vs/workbench/contrib/markers/browser/messages.ts @@ -33,7 +33,7 @@ export default class Messages { public static MARKERS_PANEL_ACTION_TOOLTIP_FILTER: string = nls.localize('markers.panel.action.filter', "Filter Problems"); public static MARKERS_PANEL_ACTION_TOOLTIP_QUICKFIX: string = nls.localize('markers.panel.action.quickfix', "Show fixes"); public static MARKERS_PANEL_FILTER_ARIA_LABEL: string = nls.localize('markers.panel.filter.ariaLabel', "Filter Problems"); - public static MARKERS_PANEL_FILTER_PLACEHOLDER: string = nls.localize('markers.panel.filter.placeholder', "Filter. E.g.: text, **/*.ts, !**/node_modules/**"); + public static MARKERS_PANEL_FILTER_PLACEHOLDER: string = nls.localize('markers.panel.filter.placeholder', "Filter (e.g. text, **/*.ts, !**/node_modules/**)"); public static MARKERS_PANEL_FILTER_ERRORS: string = nls.localize('markers.panel.filter.errors', "errors"); public static MARKERS_PANEL_FILTER_WARNINGS: string = nls.localize('markers.panel.filter.warnings', "warnings"); public static MARKERS_PANEL_FILTER_INFOS: string = nls.localize('markers.panel.filter.infos', "infos"); diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts index b6b383674be..245f72a8786 100644 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ b/src/vs/workbench/contrib/notebook/browser/constants.ts @@ -13,8 +13,8 @@ export const CELL_RUN_GUTTER = 28; export const CODE_CELL_LEFT_MARGIN = 32; export const EDITOR_TOOLBAR_HEIGHT = 0; -export const BOTTOM_CELL_TOOLBAR_HEIGHT = 18; -export const BOTTOM_CELL_TOOLBAR_OFFSET = 12; +export const BOTTOM_CELL_TOOLBAR_GAP = 18; +export const BOTTOM_CELL_TOOLBAR_HEIGHT = 50; export const CELL_STATUSBAR_HEIGHT = 22; // Margin above editor @@ -24,6 +24,7 @@ export const CELL_BOTTOM_MARGIN = 6; // Top and bottom padding inside the monaco editor in a cell, which are included in `cell.editorHeight` export const EDITOR_TOP_PADDING = 12; export const EDITOR_BOTTOM_PADDING = 4; +export const EDITOR_BOTTOM_PADDING_WITHOUT_STATUSBAR = 12; export const CELL_OUTPUT_PADDING = 14; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index 0a0a8fb1f3f..fdd17f4c6a0 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -18,7 +18,7 @@ import { InputFocusedContext, InputFocusedContextKey } from 'vs/platform/context import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; -import { BaseCellRenderTemplate, CellEditState, CellFocusMode, ICellViewModel, INotebookEditor, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED, EXPAND_CELL_CONTENT_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { BaseCellRenderTemplate, CellEditState, CellFocusMode, ICellViewModel, INotebookEditor, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED, EXPAND_CELL_CONTENT_COMMAND_ID, NOTEBOOK_CELL_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { CellKind, CellUri, NotebookCellRunState, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -38,8 +38,10 @@ const RENDER_ALL_MARKDOWN_CELLS = 'notebook.renderAllMarkdownCells'; // Cell Commands const INSERT_CODE_CELL_ABOVE_COMMAND_ID = 'notebook.cell.insertCodeCellAbove'; const INSERT_CODE_CELL_BELOW_COMMAND_ID = 'notebook.cell.insertCodeCellBelow'; +const INSERT_CODE_CELL_AT_TOP_COMMAND_ID = 'notebook.cell.insertCodeCellAtTop'; const INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID = 'notebook.cell.insertMarkdownCellAbove'; const INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID = 'notebook.cell.insertMarkdownCellBelow'; +const INSERT_MARKDOWN_CELL_AT_TOP_COMMAND_ID = 'notebook.cell.insertMarkdownCellAtTop'; const CHANGE_CELL_TO_CODE_COMMAND_ID = 'notebook.cell.changeToCode'; const CHANGE_CELL_TO_MARKDOWN_COMMAND_ID = 'notebook.cell.changeToMarkdown'; @@ -85,8 +87,14 @@ const enum CellToolbarOrder { EditCell, SplitCell, SaveCell, - ClearCellOutput, - DeleteCell + ClearCellOutput +} + +const enum CellOverflowToolbarGroups { + Copy = '1_copy', + Insert = '2_insert', + Edit = '3_edit', + Collapse = '4_collapse', } export interface INotebookActionContext { @@ -252,6 +260,23 @@ export class CancelCellAction extends MenuItemAction { } } +export class DeleteCellAction extends MenuItemAction { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService + ) { + super( + { + id: DELETE_CELL_COMMAND_ID, + title: localize('notebookActions.deleteCell', "Delete Cell"), + icon: { id: 'codicon/trash' } + }, + undefined, + { shouldForwardArgs: true }, + contextKeyService, + commandService); + } +} registerAction2(class extends NotebookCellAction { constructor() { @@ -373,6 +398,19 @@ registerAction2(class extends NotebookAction { } }); +MenuRegistry.appendMenuItem(MenuId.NotebookCellTitle, { + submenu: MenuId.NotebookCellInsert, + title: localize('notebookMenu.insertCell', "Insert Cell"), + group: CellOverflowToolbarGroups.Insert +}); + +MenuRegistry.appendMenuItem(MenuId.EditorContext, { + submenu: MenuId.NotebookCellTitle, + title: localize('notebookMenu.cellTitle', "Notebook Cell"), + group: CellOverflowToolbarGroups.Insert, + when: NOTEBOOK_EDITOR_FOCUSED +}); + MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: EXECUTE_NOTEBOOK_COMMAND_ID, @@ -409,7 +447,7 @@ registerAction2(class extends NotebookCellAction { menu: { id: MenuId.NotebookCellTitle, when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_TYPE.isEqualTo('markdown')), - group: '2_edit', + group: CellOverflowToolbarGroups.Edit, } }); } @@ -433,7 +471,7 @@ registerAction2(class extends NotebookCellAction { menu: { id: MenuId.NotebookCellTitle, when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_TYPE.isEqualTo('code')), - group: '2_edit', + group: CellOverflowToolbarGroups.Edit, } }); } @@ -526,6 +564,10 @@ registerAction2(class extends InsertCellCommand { when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, InputFocusedContext.toNegated()), weight: KeybindingWeight.WorkbenchContrib }, + menu: { + id: MenuId.NotebookCellInsert, + order: 0 + } }, CellKind.Code, 'above'); @@ -543,12 +585,66 @@ registerAction2(class extends InsertCellCommand { when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, InputFocusedContext.toNegated()), weight: KeybindingWeight.WorkbenchContrib }, + menu: { + id: MenuId.NotebookCellInsert, + order: 1 + } }, CellKind.Code, 'below'); } }); +registerAction2(class extends NotebookAction { + constructor() { + super( + { + id: INSERT_CODE_CELL_AT_TOP_COMMAND_ID, + title: localize('notebookActions.insertCodeCellAtTop', "Add Code Cell At Top"), + f1: false + }); + } + + async run(accessor: ServicesAccessor): Promise { + const context = this.getActiveEditorContext(accessor); + if (context) { + this.runWithContext(accessor, context); + } + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { + const newCell = context.notebookEditor.insertNotebookCell(undefined, CellKind.Code, 'above', undefined, true); + if (newCell) { + context.notebookEditor.focusNotebookCell(newCell, 'editor'); + } + } +}); + +registerAction2(class extends NotebookAction { + constructor() { + super( + { + id: INSERT_MARKDOWN_CELL_AT_TOP_COMMAND_ID, + title: localize('notebookActions.insertMarkdownCellAtTop', "Add Markdown Cell At Top"), + f1: false + }); + } + + async run(accessor: ServicesAccessor): Promise { + const context = this.getActiveEditorContext(accessor); + if (context) { + this.runWithContext(accessor, context); + } + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { + const newCell = context.notebookEditor.insertNotebookCell(undefined, CellKind.Markdown, 'above', undefined, true); + if (newCell) { + context.notebookEditor.focusNotebookCell(newCell, 'editor'); + } + } +}); + MenuRegistry.appendMenuItem(MenuId.NotebookCellBetween, { command: { id: INSERT_CODE_CELL_BELOW_COMMAND_ID, @@ -559,12 +655,26 @@ MenuRegistry.appendMenuItem(MenuId.NotebookCellBetween, { group: 'inline' }); +MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { + command: { + id: INSERT_CODE_CELL_AT_TOP_COMMAND_ID, + title: localize('notebookActions.menu.insertCode', "$(add) Code"), + tooltip: localize('notebookActions.menu.insertCode.tooltip', "Add Code Cell") + }, + order: 0, + group: 'inline' +}); + registerAction2(class extends InsertCellCommand { constructor() { super( { id: INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID, title: localize('notebookActions.insertMarkdownCellAbove', "Insert Markdown Cell Above"), + menu: { + id: MenuId.NotebookCellInsert, + order: 2 + } }, CellKind.Markdown, 'above'); @@ -577,6 +687,10 @@ registerAction2(class extends InsertCellCommand { { id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, title: localize('notebookActions.insertMarkdownCellBelow', "Insert Markdown Cell Below"), + menu: { + id: MenuId.NotebookCellInsert, + order: 3 + } }, CellKind.Markdown, 'below'); @@ -593,6 +707,16 @@ MenuRegistry.appendMenuItem(MenuId.NotebookCellBetween, { group: 'inline' }); +MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { + command: { + id: INSERT_MARKDOWN_CELL_AT_TOP_COMMAND_ID, + title: localize('notebookActions.menu.insertMarkdown', "$(add) Markdown"), + tooltip: localize('notebookActions.menu.insertMarkdown.tooltip', "Add Markdown Cell") + }, + order: 1, + group: 'inline' +}); + registerAction2(class extends NotebookCellAction { constructor() { super( @@ -667,9 +791,7 @@ registerAction2(class extends NotebookCellAction { title: localize('notebookActions.deleteCell', "Delete Cell"), menu: { id: MenuId.NotebookCellTitle, - order: CellToolbarOrder.DeleteCell, - when: NOTEBOOK_EDITOR_EDITABLE, - group: CELL_TITLE_CELL_GROUP_ID + when: NOTEBOOK_EDITOR_EDITABLE }, keybinding: { primary: KeyCode.Delete, @@ -766,7 +888,7 @@ registerAction2(class extends NotebookCellAction { menu: { id: MenuId.NotebookCellTitle, when: NOTEBOOK_EDITOR_FOCUSED, - group: '1_copy', + group: CellOverflowToolbarGroups.Copy, } }); } @@ -788,7 +910,7 @@ registerAction2(class extends NotebookCellAction { menu: { id: MenuId.NotebookCellTitle, when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), - group: '1_copy', + group: CellOverflowToolbarGroups.Copy, } }); } @@ -817,7 +939,7 @@ registerAction2(class extends NotebookAction { menu: { id: MenuId.NotebookCellTitle, when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE), - group: '1_copy', + group: CellOverflowToolbarGroups.Copy, } }); } @@ -1146,10 +1268,10 @@ registerAction2(class extends NotebookCellAction { constructor() { super({ id: CLEAR_CELL_OUTPUTS_COMMAND_ID, - title: localize('clearActiveCellOutputs', 'Clear Active Cell Outputs'), + title: localize('clearCellOutputs', 'Clear Cell Outputs'), menu: { id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_CELL_TYPE.isEqualTo('code'), NOTEBOOK_EDITOR_RUNNABLE), + when: ContextKeyExpr.and(NOTEBOOK_CELL_TYPE.isEqualTo('code'), NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_CELL_HAS_OUTPUTS), order: CellToolbarOrder.ClearCellOutput, group: CELL_TITLE_OUTPUT_GROUP_ID }, @@ -1169,6 +1291,14 @@ registerAction2(class extends NotebookCellAction { } editor.viewModel.notebookDocument.clearCellOutput(context.cell.handle); + if (context.cell.metadata && context.cell.metadata?.runState !== NotebookCellRunState.Running) { + context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { + runState: NotebookCellRunState.Idle, + runStartTime: undefined, + lastRunDuration: undefined, + statusMessage: undefined + }); + } } }); @@ -1200,7 +1330,7 @@ export class ChangeCellLanguageAction extends NotebookCellAction { const providerLanguages = [...context.notebookEditor.viewModel!.notebookDocument.languages, 'markdown']; providerLanguages.forEach(languageId => { let description: string; - if (languageId === context.cell.language) { + if (context.cell.cellKind === CellKind.Markdown ? (languageId === 'markdown') : (languageId === context.cell.language)) { description = localize('languageDescription', "({0}) - Current Language", languageId); } else { description = localize('languageDescriptionConfigured', "({0})", languageId); @@ -1312,7 +1442,7 @@ registerAction2(class extends NotebookCellAction { title: localize('notebookActions.splitCell', "Split Cell"), menu: { id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, InputFocusedContext), + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_FOCUSED, InputFocusedContext), order: CellToolbarOrder.SplitCell, group: CELL_TITLE_CELL_GROUP_ID, // alt: { @@ -1352,6 +1482,12 @@ registerAction2(class extends NotebookCellAction { when: NOTEBOOK_EDITOR_FOCUSED, primary: KeyMod.WinCtrl | KeyMod.Alt | KeyMod.Shift | KeyCode.KEY_J, weight: KeybindingWeight.WorkbenchContrib + }, + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), + group: CellOverflowToolbarGroups.Edit, + order: 10 } }); } @@ -1375,7 +1511,8 @@ registerAction2(class extends NotebookCellAction { menu: { id: MenuId.NotebookCellTitle, when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), - group: '2_edit', + group: CellOverflowToolbarGroups.Edit, + order: 11 } }); } @@ -1413,19 +1550,19 @@ registerAction2(class extends NotebookCellAction { title: localize('notebookActions.collapseCellInput', "Collapse Cell Input"), keybinding: { when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_INPUT_COLLAPSED.toNegated(), InputFocusedContext.toNegated()), - primary: KeyChord(KeyCode.KEY_C, KeyCode.KEY_C), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_C), weight: KeybindingWeight.WorkbenchContrib }, menu: { id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_INPUT_COLLAPSED.toNegated()), - group: '3_collapse', + when: ContextKeyExpr.and(NOTEBOOK_CELL_INPUT_COLLAPSED.toNegated()), + group: CellOverflowToolbarGroups.Collapse, } }); } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { inputCollapsed: true }); + context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { inputCollapsed: true }); } }); @@ -1436,19 +1573,19 @@ registerAction2(class extends NotebookCellAction { title: localize('notebookActions.expandCellContent', "Expand Cell Content"), keybinding: { when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_INPUT_COLLAPSED), - primary: KeyChord(KeyCode.KEY_C, KeyCode.KEY_C), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_C), weight: KeybindingWeight.WorkbenchContrib }, menu: { id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_INPUT_COLLAPSED), - group: '3_collapse', + when: ContextKeyExpr.and(NOTEBOOK_CELL_INPUT_COLLAPSED), + group: CellOverflowToolbarGroups.Collapse, } }); } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { inputCollapsed: false }); + context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { inputCollapsed: false }); } }); @@ -1459,19 +1596,19 @@ registerAction2(class extends NotebookCellAction { title: localize('notebookActions.collapseCellOutput', "Collapse Cell Output"), keybinding: { when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED.toNegated(), InputFocusedContext.toNegated(), NOTEBOOK_CELL_HAS_OUTPUTS), - primary: KeyChord(KeyCode.KEY_C, KeyCode.KEY_O), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_T), weight: KeybindingWeight.WorkbenchContrib }, menu: { id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED.toNegated(), NOTEBOOK_CELL_HAS_OUTPUTS), - group: '3_collapse', + when: ContextKeyExpr.and(NOTEBOOK_CELL_OUTPUT_COLLAPSED.toNegated(), NOTEBOOK_CELL_HAS_OUTPUTS), + group: CellOverflowToolbarGroups.Collapse, } }); } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { outputCollapsed: true }); + context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { outputCollapsed: true }); } }); @@ -1482,19 +1619,19 @@ registerAction2(class extends NotebookCellAction { title: localize('notebookActions.expandCellOutput', "Expand Cell Output"), keybinding: { when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED), - primary: KeyChord(KeyCode.KEY_C, KeyCode.KEY_O), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_T), weight: KeybindingWeight.WorkbenchContrib }, menu: { id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED), - group: '3_collapse', + when: ContextKeyExpr.and(NOTEBOOK_CELL_OUTPUT_COLLAPSED), + group: CellOverflowToolbarGroups.Collapse, } }); } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { outputCollapsed: false }); + context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { outputCollapsed: false }); } }); @@ -1527,7 +1664,16 @@ registerAction2(class extends Action2 { const activeEditorContext = this.getActiveEditorContext(accessor); if (activeEditorContext) { - activeEditorContext.notebookEditor.viewModel!.inspectLayout(); + const viewModel = activeEditorContext.notebookEditor.viewModel!; + console.log('--- notebook ---'); + console.log(viewModel.layoutInfo); + console.log('--- cells ---'); + for (let i = 0; i < viewModel.length; i++) { + const cell = viewModel.viewCells[i] as CellViewModel; + console.log(`--- cell: ${cell.handle} ---`); + console.log(cell.layoutInfo); + } + } } }); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts b/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts index 814dc3a2527..aa7711d6817 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { INotebookEditor, INotebookEditorMouseEvent, ICellRange, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditor, INotebookEditorMouseEvent, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import * as DOM from 'vs/base/browser/dom'; import { CellFoldingState, FoldingModel } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; -import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -18,6 +18,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { getActiveNotebookEditor, NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; import { localize } from 'vs/nls'; +import { FoldingRegion } from 'vs/editor/contrib/folding/foldingRanges'; export class FoldingController extends Disposable implements INotebookEditorContribution { static id: string = 'workbench.notebook.findController'; @@ -65,19 +66,31 @@ export class FoldingController extends Disposable implements INotebookEditorCont this._updateEditorFoldingRanges(); } - setFoldingState(index: number, state: CellFoldingState) { + setFoldingStateDown(index: number, state: CellFoldingState, levels: number) { + const doCollapse = state === CellFoldingState.Collapsed; + let region = this._foldingModel!.getRegionAtLine(index + 1); + let regions: FoldingRegion[] = []; + if (region) { + if (region.isCollapsed !== doCollapse) { + regions.push(region); + } + if (levels > 1) { + let regionsInside = this._foldingModel!.getRegionsInside(region, (r, level: number) => r.isCollapsed !== doCollapse && level < levels); + regions.push(...regionsInside); + } + } + + regions.forEach(r => this._foldingModel!.setCollapsed(r.regionIndex, state === CellFoldingState.Collapsed)); + this._updateEditorFoldingRanges(); + } + + setFoldingStateUp(index: number, state: CellFoldingState, levels: number) { if (!this._foldingModel) { return; } - const range = this._foldingModel.regions.findRange(index + 1); - const startIndex = this._foldingModel.regions.getStartLineNumber(range) - 1; - - if (startIndex !== index) { - return; - } - - this._foldingModel.setCollapsed(range, state === CellFoldingState.Collapsed); + let regions = this._foldingModel.getAllRegionsAtLine(index + 1, (region, level) => region.isCollapsed !== (state === CellFoldingState.Collapsed) && level <= levels); + regions.forEach(r => this._foldingModel!.setCollapsed(r.regionIndex, state === CellFoldingState.Collapsed)); this._updateEditorFoldingRanges(); } @@ -121,7 +134,7 @@ export class FoldingController extends Disposable implements INotebookEditorCont return; } - this.setFoldingState(modelIndex, state === CellFoldingState.Collapsed ? CellFoldingState.Expanded : CellFoldingState.Collapsed); + this.setFoldingStateUp(modelIndex, state === CellFoldingState.Collapsed ? CellFoldingState.Expanded : CellFoldingState.Collapsed, 1); } return; @@ -130,6 +143,10 @@ export class FoldingController extends Disposable implements INotebookEditorCont registerNotebookContribution(FoldingController.id, FoldingController); + +const NOTEBOOK_FOLD_COMMAND_LABEL = localize('fold.cell', "Fold Cell"); +const NOTEBOOK_UNFOLD_COMMAND_LABEL = localize('unfold.cell', "Unfold Cell"); + registerAction2(class extends Action2 { constructor() { super({ @@ -146,12 +163,39 @@ registerAction2(class extends Action2 { secondary: [KeyCode.LeftArrow], weight: KeybindingWeight.WorkbenchContrib }, + description: { + description: NOTEBOOK_FOLD_COMMAND_LABEL, + args: [ + { + name: 'index', + description: 'The cell index', + schema: { + 'type': 'object', + 'required': ['index', 'direction'], + 'properties': { + 'index': { + 'type': 'number' + }, + 'direction': { + 'type': 'string', + 'enum': ['up', 'down'], + 'default': 'down' + }, + 'levels': { + 'type': 'number', + 'default': 1 + }, + } + } + } + ] + }, precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } - async run(accessor: ServicesAccessor): Promise { + async run(accessor: ServicesAccessor, args?: { index: number, levels: number, direction: 'up' | 'down' }): Promise { const editorService = accessor.get(IEditorService); const editor = getActiveNotebookEditor(editorService); @@ -159,17 +203,27 @@ registerAction2(class extends Action2 { return; } - const activeCell = editor.getActiveCell(); - if (!activeCell) { - return; + const levels = args && args.levels || 1; + const direction = args && args.direction === 'up' ? 'up' : 'down'; + let index: number | undefined = undefined; + + if (args) { + index = args.index; + } else { + const activeCell = editor.getActiveCell(); + if (!activeCell) { + return; + } + index = editor.viewModel?.viewCells.indexOf(activeCell); } const controller = editor.getContribution(FoldingController.id); - - const index = editor.viewModel?.viewCells.indexOf(activeCell); - if (index !== undefined) { - controller.setFoldingState(index, CellFoldingState.Collapsed); + if (direction === 'up') { + controller.setFoldingStateUp(index, CellFoldingState.Collapsed, levels); + } else { + controller.setFoldingStateDown(index, CellFoldingState.Collapsed, levels); + } } } }); @@ -178,7 +232,7 @@ registerAction2(class extends Action2 { constructor() { super({ id: 'notebook.unfold', - title: { value: localize('unfold.cell', "Unfold Cell"), original: 'Unfold Cell' }, + title: { value: NOTEBOOK_UNFOLD_COMMAND_LABEL, original: 'Unfold Cell' }, category: NOTEBOOK_ACTIONS_CATEGORY, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), @@ -190,12 +244,39 @@ registerAction2(class extends Action2 { secondary: [KeyCode.RightArrow], weight: KeybindingWeight.WorkbenchContrib }, + description: { + description: NOTEBOOK_UNFOLD_COMMAND_LABEL, + args: [ + { + name: 'index', + description: 'The cell index', + schema: { + 'type': 'object', + 'required': ['index', 'direction'], + 'properties': { + 'index': { + 'type': 'number' + }, + 'direction': { + 'type': 'string', + 'enum': ['up', 'down'], + 'default': 'down' + }, + 'levels': { + 'type': 'number', + 'default': 1 + }, + } + } + } + ] + }, precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } - async run(accessor: ServicesAccessor): Promise { + async run(accessor: ServicesAccessor, args?: { index: number, levels: number, direction: 'up' | 'down' }): Promise { const editorService = accessor.get(IEditorService); const editor = getActiveNotebookEditor(editorService); @@ -203,17 +284,27 @@ registerAction2(class extends Action2 { return; } - const activeCell = editor.getActiveCell(); - if (!activeCell) { - return; + const levels = args && args.levels || 1; + const direction = args && args.direction === 'up' ? 'up' : 'down'; + let index: number | undefined = undefined; + + if (args) { + index = args.index; + } else { + const activeCell = editor.getActiveCell(); + if (!activeCell) { + return; + } + index = editor.viewModel?.viewCells.indexOf(activeCell); } const controller = editor.getContribution(FoldingController.id); - - const index = editor.viewModel?.viewCells.indexOf(activeCell); - if (index !== undefined) { - controller.setFoldingState(index, CellFoldingState.Expanded); + if (direction === 'up') { + controller.setFoldingStateUp(index, CellFoldingState.Expanded, levels); + } else { + controller.setFoldingStateDown(index, CellFoldingState.Expanded, levels); + } } } }); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts b/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts index 18f853b05e5..3675729c2dd 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts @@ -6,11 +6,14 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { TrackedRangeStickiness } from 'vs/editor/common/model'; -import { FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges'; +import { FoldingRegion, FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges'; import { IFoldingRangeData, sanitizeRanges } from 'vs/editor/contrib/folding/syntaxRangeProvider'; -import { ICellRange } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +type RegionFilter = (r: FoldingRegion) => boolean; +type RegionFilterWithLevel = (r: FoldingRegion, level: number) => boolean; + export class FoldingModel extends Disposable { private _viewModel: NotebookViewModel | null = null; @@ -73,7 +76,70 @@ export class FoldingModel extends Disposable { this.recompute(); } - public setCollapsed(index: number, newState: boolean) { + getRegionAtLine(lineNumber: number): FoldingRegion | null { + if (this._regions) { + let index = this._regions.findRange(lineNumber); + if (index >= 0) { + return this._regions.toRegion(index); + } + } + return null; + } + + getRegionsInside(region: FoldingRegion | null, filter?: RegionFilter | RegionFilterWithLevel): FoldingRegion[] { + let result: FoldingRegion[] = []; + let index = region ? region.regionIndex + 1 : 0; + let endLineNumber = region ? region.endLineNumber : Number.MAX_VALUE; + + if (filter && filter.length === 2) { + const levelStack: FoldingRegion[] = []; + for (let i = index, len = this._regions.length; i < len; i++) { + let current = this._regions.toRegion(i); + if (this._regions.getStartLineNumber(i) < endLineNumber) { + while (levelStack.length > 0 && !current.containedBy(levelStack[levelStack.length - 1])) { + levelStack.pop(); + } + levelStack.push(current); + if (filter(current, levelStack.length)) { + result.push(current); + } + } else { + break; + } + } + } else { + for (let i = index, len = this._regions.length; i < len; i++) { + let current = this._regions.toRegion(i); + if (this._regions.getStartLineNumber(i) < endLineNumber) { + if (!filter || (filter as RegionFilter)(current)) { + result.push(current); + } + } else { + break; + } + } + } + return result; + } + + getAllRegionsAtLine(lineNumber: number, filter?: (r: FoldingRegion, level: number) => boolean): FoldingRegion[] { + let result: FoldingRegion[] = []; + if (this._regions) { + let index = this._regions.findRange(lineNumber); + let level = 1; + while (index >= 0) { + let current = this._regions.toRegion(index); + if (!filter || filter(current, level)) { + result.push(current); + } + level++; + index = current.parentIndex; + } + } + return result; + } + + setCollapsed(index: number, newState: boolean) { this._regions.setCollapsed(index, newState); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts b/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts index 75400be17fb..51d77a18fbd 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; +import { setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { FoldingModel } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; @@ -17,7 +16,7 @@ function updateFoldingStateAtIndex(foldingModel: FoldingModel, index: number, co } suite('Notebook Folding', () => { - const instantiationService = new TestInstantiationService(); + const instantiationService = setupInstantiationService(); const blukEditService = instantiationService.get(IBulkEditService); const undoRedoService = instantiationService.stub(IUndoRedoService, () => { }); instantiationService.spy(IUndoRedoService, 'pushElement'); @@ -28,13 +27,13 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.1'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.1', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingController = new FoldingModel(); @@ -57,13 +56,13 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.1\n# header3'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.1\n# header3', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingController = new FoldingModel(); @@ -91,13 +90,13 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.1'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.1', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -115,13 +114,13 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -140,13 +139,13 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -167,13 +166,13 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -224,18 +223,18 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -255,18 +254,18 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -290,18 +289,18 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -327,18 +326,18 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -366,18 +365,18 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts index 6cb8b4a2bb0..446f4940e2a 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts @@ -17,8 +17,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { getDocumentFormattingEditsUntilResult, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; -import { WorkspaceTextEdit } from 'vs/editor/common/modes'; +import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { registerEditorAction, EditorAction } from 'vs/editor/browser/editorExtensions'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -63,7 +62,7 @@ registerAction2(class extends Action2 { const dispoables = new DisposableStore(); try { - const edits: WorkspaceTextEdit[] = []; + const edits: ResourceTextEdit[] = []; for (const cell of notebook.cells) { @@ -78,18 +77,13 @@ registerAction2(class extends Action2 { ); if (formatEdits) { - formatEdits.forEach(edit => edits.push({ - edit, - resource: model.uri, - modelVersionId: model.getVersionId() - })); + for (let edit of formatEdits) { + edits.push(new ResourceTextEdit(model.uri, edit, model.getVersionId())); + } } } - await bulkEditService.apply( - { edits }, - { label: localize('label', "Format Notebook") } - ); + await bulkEditService.apply(edits, { label: localize('label', "Format Notebook") }); } finally { dispoables.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts b/src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts new file mode 100644 index 00000000000..3b3601c8d7f --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { INotebookEditorContribution, INotebookEditor } from '../../notebookBrowser'; +import { registerNotebookContribution } from '../../notebookEditorExtensions'; +import { ISCMService } from 'vs/workbench/contrib/scm/common/scm'; +import { createProviderComparer } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator'; +import { first, ThrottledDelayer } from 'vs/base/common/async'; +import { INotebookService } from '../../../common/notebookService'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { URI } from 'vs/base/common/uri'; + +export class SCMController extends Disposable implements INotebookEditorContribution { + static id: string = 'workbench.notebook.findController'; + private _lastDecorationId: string[] = []; + private _localDisposable = new DisposableStore(); + private _originalDocument: NotebookTextModel | undefined = undefined; + private _originalResourceDisposableStore = new DisposableStore(); + private _diffDelayer = new ThrottledDelayer(200); + + private _lastVersion = -1; + + + constructor( + private readonly _notebookEditor: INotebookEditor, + @IFileService private readonly _fileService: FileService, + @ISCMService private readonly _scmService: ISCMService, + @INotebookService private readonly _notebookService: INotebookService + + ) { + super(); + + if (!this._notebookEditor.isEmbedded) { + this._register(this._notebookEditor.onDidChangeModel(() => { + this._localDisposable.clear(); + this._originalResourceDisposableStore.clear(); + this._diffDelayer.cancel(); + this.update(); + + if (this._notebookEditor.textModel) { + this._localDisposable.add(this._notebookEditor.textModel.onDidChangeContent(() => { + this.update(); + })); + + this._localDisposable.add(this._notebookEditor.textModel.onDidChangeCells(() => { + this.update(); + })); + } + })); + + this._register(this._notebookEditor.onWillDispose(() => { + this._localDisposable.clear(); + this._originalResourceDisposableStore.clear(); + })); + + this.update(); + } + } + + private async _resolveNotebookDocument(uri: URI, viewType: string) { + const providers = this._scmService.repositories.map(r => r.provider); + const rootedProviders = providers.filter(p => !!p.rootUri); + + rootedProviders.sort(createProviderComparer(uri)); + + const result = await first(rootedProviders.map(p => () => p.getOriginalResource(uri))); + + if (!result) { + this._originalDocument = undefined; + this._originalResourceDisposableStore.clear(); + return; + } + + if (result.toString() === this._originalDocument?.uri.toString()) { + // original document not changed + return; + } + + this._originalResourceDisposableStore.add(this._fileService.watch(result)); + this._originalResourceDisposableStore.add(this._fileService.onDidFilesChange(e => { + if (e.changes.find(change => change.resource.toString() === result.toString())) { + this._originalDocument = undefined; + this._originalResourceDisposableStore.clear(); + this.update(); + } + })); + + const originalDocument = await this._notebookService.resolveNotebook(viewType, result, false); + this._originalResourceDisposableStore.add({ + dispose: () => { + this._originalDocument?.dispose(); + this._originalDocument = undefined; + } + }); + + this._originalDocument = originalDocument; + } + + async update() { + if (!this._diffDelayer) { + return; + } + + await this._diffDelayer + .trigger(async () => { + const modifiedDocument = this._notebookEditor.textModel; + if (!modifiedDocument) { + return; + } + + if (this._lastVersion >= modifiedDocument.versionId) { + return; + } + + this._lastVersion = modifiedDocument.versionId; + await this._resolveNotebookDocument(modifiedDocument.uri, modifiedDocument.viewType); + + if (!this._originalDocument) { + this._clear(); + return; + } + + // const diff = new LcsDiff(new CellSequence(this._originalDocument), new CellSequence(modifiedDocument)); + // const diffResult = diff.ComputeDiff(false); + + // const decorations: INotebookDeltaDecoration[] = []; + // diffResult.changes.forEach(change => { + // if (change.originalLength === 0) { + // // doesn't exist in original + // for (let i = 0; i < change.modifiedLength; i++) { + // decorations.push({ + // handle: modifiedDocument.cells[change.modifiedStart + i].handle, + // options: { gutterClassName: 'nb-gutter-cell-inserted' } + // }); + // } + // } else { + // if (change.modifiedLength === 0) { + // // diff.deleteCount + // // removed from original + // } else { + // // modification + // for (let i = 0; i < change.modifiedLength; i++) { + // decorations.push({ + // handle: modifiedDocument.cells[change.modifiedStart + i].handle, + // options: { gutterClassName: 'nb-gutter-cell-changed' } + // }); + // } + // } + // } + // }); + + + // this._lastDecorationId = this._notebookEditor.deltaCellDecorations(this._lastDecorationId, decorations); + }); + } + + private _clear() { + this._lastDecorationId = this._notebookEditor.deltaCellDecorations(this._lastDecorationId, []); + } +} + +registerNotebookContribution(SCMController.id, SCMController); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts index bb6be90bb17..2c2c937e6cf 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts @@ -14,7 +14,7 @@ import { INotebookEditor, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { INotebookKernelInfo2, INotebookKernelInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookKernelInfo2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; @@ -50,24 +50,22 @@ registerAction2(class extends Action2 { const tokenSource = new CancellationTokenSource(); const availableKernels2 = await notebookService.getContributedNotebookKernels2(editor.viewModel!.viewType, editor.viewModel!.uri, tokenSource.token); - const availableKernels = notebookService.getContributedNotebookKernels(editor.viewModel!.viewType, editor.viewModel!.uri); - const picks: QuickPickInput[] = [...availableKernels2, ...availableKernels].map((a) => { + const picks: QuickPickInput[] = [...availableKernels2].map((a) => { return { id: a.id, label: a.label, picked: a.id === activeKernel?.id, description: - (a as INotebookKernelInfo2).description - ? (a as INotebookKernelInfo2).description + a.description + ? a.description : a.extension.value + (a.id === activeKernel?.id ? nls.localize('currentActiveKernel', " (Currently Active)") : ''), + detail: a.detail, kernelProviderId: a.extension.value, run: async () => { editor.activeKernel = a; - if ((a as any).resolve) { - (a as INotebookKernelInfo2).resolve(editor.uri!, editor.getId(), tokenSource.token); - } + a.resolve(editor.uri!, editor.getId(), tokenSource.token); }, buttons: [{ iconClass: 'codicon-settings-gear', @@ -76,27 +74,6 @@ registerAction2(class extends Action2 { }; }); - const provider = notebookService.getContributedNotebookProviders(editor.viewModel!.uri)[0]; - - if (provider.kernel) { - picks.unshift({ - id: provider.id, - label: provider.displayName, - picked: !activeKernel, // no active kernel, the builtin kernel of the provider is used - description: activeKernel === undefined - ? nls.localize('currentActiveBuiltinKernel', " (Currently Active)") - : '', - kernelProviderId: provider.providerExtensionId, - run: () => { - editor.activeKernel = undefined; - }, - buttons: [{ - iconClass: 'codicon-settings-gear', - tooltip: nls.localize('notebook.promptKernel.setDefaultTooltip', "Set as default kernel provider for '{0}'", editor.viewModel!.viewType) - }] - }); - } - const picker = quickInputService.createQuickPick<(IQuickPickItem & { run(): void; kernelProviderId?: string })>(); picker.items = picks; picker.activeItems = picks.filter(pick => (pick as IQuickPickItem).picked) as (IQuickPickItem & { run(): void; kernelProviderId?: string; })[]; @@ -192,7 +169,7 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { } } - showKernelStatus(kernel: INotebookKernelInfo | INotebookKernelInfo2 | undefined) { + showKernelStatus(kernel: INotebookKernelInfo2 | undefined) { this.kernelInfoElement.value = this._statusbarService.addEntry({ text: kernel ? kernel.label : 'Choose Kernel', ariaLabel: kernel ? kernel.label : 'Choose Kernel', diff --git a/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts new file mode 100644 index 00000000000..2c938b1c3ae --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts @@ -0,0 +1,929 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { CellDiffViewModel, PropertyFoldingState } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { CellDiffRenderTemplate, CellDiffViewModelLayoutChangeEvent, DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common'; +import { EDITOR_BOTTOM_PADDING, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; +import { renderCodicons } from 'vs/base/common/codicons'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { format } from 'vs/base/common/jsonFormatter'; +import { applyEdits } from 'vs/base/common/jsonEdit'; +import { CellUri, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { hash } from 'vs/base/common/hash'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IMenu, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IAction } from 'vs/base/common/actions'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; + +const fixedEditorOptions: IEditorOptions = { + padding: { + top: 12, + bottom: 12 + }, + scrollBeyondLastLine: false, + scrollbar: { + verticalScrollbarSize: 14, + horizontal: 'auto', + useShadows: true, + verticalHasArrows: false, + horizontalHasArrows: false, + alwaysConsumeMouseWheel: false + }, + renderLineHighlightOnlyWhenFocus: true, + overviewRulerLanes: 0, + selectOnLineNumbers: false, + wordWrap: 'off', + lineNumbers: 'off', + lineDecorationsWidth: 0, + glyphMargin: false, + fixedOverflowWidgets: true, + minimap: { enabled: false }, + renderValidationDecorations: 'on', + renderLineHighlight: 'none', + readOnly: true +}; + +const fixedDiffEditorOptions: IDiffEditorOptions = { + ...fixedEditorOptions, + glyphMargin: true, + enableSplitViewResizing: false, + renderIndicators: false, + readOnly: false +}; + + + +class PropertyHeader extends Disposable { + protected _foldingIndicator!: HTMLElement; + protected _statusSpan!: HTMLElement; + protected _toolbar!: ToolBar; + protected _menu!: IMenu; + + constructor( + readonly cell: CellDiffViewModel, + readonly metadataHeaderContainer: HTMLElement, + readonly notebookEditor: INotebookTextDiffEditor, + readonly accessor: { + updateInfoRendering: () => void; + checkIfModified: (cell: CellDiffViewModel) => boolean; + getFoldingState: (cell: CellDiffViewModel) => PropertyFoldingState; + updateFoldingState: (cell: CellDiffViewModel, newState: PropertyFoldingState) => void; + unChangedLabel: string; + changedLabel: string; + prefix: string; + menuId: MenuId; + }, + @IContextMenuService readonly contextMenuService: IContextMenuService, + @IKeybindingService readonly keybindingService: IKeybindingService, + @INotificationService readonly notificationService: INotificationService, + @IMenuService readonly menuService: IMenuService, + @IContextKeyService readonly contextKeyService: IContextKeyService + ) { + super(); + } + + buildHeader(): void { + let metadataChanged = this.accessor.checkIfModified(this.cell); + this._foldingIndicator = DOM.append(this.metadataHeaderContainer, DOM.$('.property-folding-indicator')); + DOM.addClass(this._foldingIndicator, this.accessor.prefix); + this._updateFoldingIcon(); + const metadataStatus = DOM.append(this.metadataHeaderContainer, DOM.$('div.property-status')); + this._statusSpan = DOM.append(metadataStatus, DOM.$('span')); + + if (metadataChanged) { + this._statusSpan.textContent = this.accessor.changedLabel; + this._statusSpan.style.fontWeight = 'bold'; + DOM.addClass(this.metadataHeaderContainer, 'modified'); + } else { + this._statusSpan.textContent = this.accessor.unChangedLabel; + } + + const cellToolbarContainer = DOM.append(this.metadataHeaderContainer, DOM.$('div.property-toolbar')); + this._toolbar = new ToolBar(cellToolbarContainer, this.contextMenuService, { + actionViewItemProvider: action => { + if (action instanceof MenuItemAction) { + const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + return item; + } + + return undefined; + } + }); + this._toolbar.context = { + cell: this.cell + }; + + this._menu = this.menuService.createMenu(this.accessor.menuId, this.contextKeyService); + + if (metadataChanged) { + const actions: IAction[] = []; + createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions); + this._toolbar.setActions(actions); + } + + this._register(this.notebookEditor.onMouseUp(e => { + if (!e.event.target) { + return; + } + + const target = e.event.target as HTMLElement; + + if (DOM.hasClass(target, 'codicon-chevron-down') || DOM.hasClass(target, 'codicon-chevron-right')) { + const parent = target.parentElement as HTMLElement; + + if (!parent) { + return; + } + + if (!DOM.hasClass(parent, this.accessor.prefix)) { + return; + } + + if (!DOM.hasClass(parent, 'property-folding-indicator')) { + return; + } + + // folding icon + + const cellViewModel = e.target; + + if (cellViewModel === this.cell) { + const oldFoldingState = this.accessor.getFoldingState(this.cell); + this.accessor.updateFoldingState(this.cell, oldFoldingState === PropertyFoldingState.Expanded ? PropertyFoldingState.Collapsed : PropertyFoldingState.Expanded); + this._updateFoldingIcon(); + this.accessor.updateInfoRendering(); + } + } + + return; + })); + + this._updateFoldingIcon(); + this.accessor.updateInfoRendering(); + } + + refresh() { + let metadataChanged = this.accessor.checkIfModified(this.cell); + if (metadataChanged) { + this._statusSpan.textContent = this.accessor.changedLabel; + this._statusSpan.style.fontWeight = 'bold'; + DOM.addClass(this.metadataHeaderContainer, 'modified'); + const actions: IAction[] = []; + createAndFillInActionBarActions(this._menu, undefined, actions); + this._toolbar.setActions(actions); + } else { + this._statusSpan.textContent = this.accessor.unChangedLabel; + this._statusSpan.style.fontWeight = 'normal'; + this._toolbar.setActions([]); + } + } + + private _updateFoldingIcon() { + if (this.accessor.getFoldingState(this.cell) === PropertyFoldingState.Collapsed) { + this._foldingIndicator.innerHTML = renderCodicons('$(chevron-right)'); + } else { + this._foldingIndicator.innerHTML = renderCodicons('$(chevron-down)'); + } + } +} + +abstract class AbstractCellRenderer extends Disposable { + protected _metadataHeaderContainer!: HTMLElement; + protected _metadataHeader!: PropertyHeader; + protected _metadataInfoContainer!: HTMLElement; + protected _metadataEditorContainer?: HTMLElement; + protected _metadataEditorDisposeStore!: DisposableStore; + protected _metadataEditor?: CodeEditorWidget | DiffEditorWidget; + + protected _outputHeaderContainer!: HTMLElement; + protected _outputHeader!: PropertyHeader; + protected _outputInfoContainer!: HTMLElement; + protected _outputEditorContainer?: HTMLElement; + protected _outputEditorDisposeStore!: DisposableStore; + protected _outputEditor?: CodeEditorWidget | DiffEditorWidget; + + + protected _diffEditorContainer!: HTMLElement; + protected _diagonalFill?: HTMLElement; + protected _layoutInfo!: { + editorHeight: number; + editorMargin: number; + metadataStatusHeight: number; + metadataHeight: number; + outputStatusHeight: number; + outputHeight: number; + bodyMargin: number; + }; + + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: CellDiffViewModel, + readonly templateData: CellDiffRenderTemplate, + readonly style: 'left' | 'right' | 'full', + protected readonly instantiationService: IInstantiationService, + protected readonly modeService: IModeService, + protected readonly modelService: IModelService, + + ) { + super(); + // init + this._layoutInfo = { + editorHeight: 0, + editorMargin: 0, + metadataHeight: 0, + metadataStatusHeight: 25, + outputHeight: 0, + outputStatusHeight: 25, + bodyMargin: 32 + }; + this._metadataEditorDisposeStore = new DisposableStore(); + this._outputEditorDisposeStore = new DisposableStore(); + this._register(this._metadataEditorDisposeStore); + this.initData(); + this.buildBody(templateData.container); + this._register(cell.onDidLayoutChange(e => this.onDidLayoutChange(e))); + } + + buildBody(container: HTMLElement) { + const body = DOM.$('.cell-body'); + DOM.append(container, body); + this._diffEditorContainer = DOM.$('.cell-diff-editor-container'); + switch (this.style) { + case 'left': + DOM.addClass(body, 'left'); + break; + case 'right': + DOM.addClass(body, 'right'); + break; + default: + DOM.addClass(body, 'full'); + break; + } + + DOM.append(body, this._diffEditorContainer); + this._diagonalFill = DOM.append(body, DOM.$('.diagonal-fill')); + this.styleContainer(this._diffEditorContainer); + const sourceContainer = DOM.append(this._diffEditorContainer, DOM.$('.source-container')); + this.buildSourceEditor(sourceContainer); + + this._metadataHeaderContainer = DOM.append(this._diffEditorContainer, DOM.$('.metadata-header-container')); + this._metadataInfoContainer = DOM.append(this._diffEditorContainer, DOM.$('.metadata-info-container')); + + this._metadataHeader = this.instantiationService.createInstance( + PropertyHeader, + this.cell, + this._metadataHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: this.updateMetadataRendering.bind(this), + checkIfModified: (cell) => { + return cell.type !== 'delete' && cell.type !== 'insert' && hash(this._getFormatedMetadataJSON(cell.original?.metadata || {}, cell.original?.language)) !== hash(this._getFormatedMetadataJSON(cell.modified?.metadata ?? {}, cell.modified?.language)); + }, + getFoldingState: (cell) => { + return cell.metadataFoldingState; + }, + updateFoldingState: (cell, state) => { + cell.metadataFoldingState = state; + }, + unChangedLabel: 'Metadata', + changedLabel: 'Metadata changed', + prefix: 'metadata', + menuId: MenuId.NotebookDiffCellMetadataTitle + } + ); + this._register(this._metadataHeader); + this._metadataHeader.buildHeader(); + + this._outputHeaderContainer = DOM.append(this._diffEditorContainer, DOM.$('.output-header-container')); + this._outputInfoContainer = DOM.append(this._diffEditorContainer, DOM.$('.output-info-container')); + + this._outputHeader = this.instantiationService.createInstance( + PropertyHeader, + this.cell, + this._outputHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: this.updateOutputRendering.bind(this), + checkIfModified: (cell) => { + return cell.type !== 'delete' && cell.type !== 'insert' && !this.notebookEditor.textModel!.transientOptions.transientOutputs && cell.type === 'modified' && hash(cell.original?.outputs ?? []) !== hash(cell.modified?.outputs ?? []); + }, + getFoldingState: (cell) => { + return this.cell.outputFoldingState; + }, + updateFoldingState: (cell, state) => { + cell.outputFoldingState = state; + }, + unChangedLabel: 'Outputs', + changedLabel: 'Outputs changed', + prefix: 'output', + menuId: MenuId.NotebookDiffCellOutputsTitle + } + ); + this._register(this._outputHeader); + this._outputHeader.buildHeader(); + } + + updateMetadataRendering() { + if (this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { + // we should expand the metadata editor + this._metadataInfoContainer.style.display = 'block'; + + if (!this._metadataEditorContainer || !this._metadataEditor) { + // create editor + this._metadataEditorContainer = DOM.append(this._metadataInfoContainer, DOM.$('.metadata-editor-container')); + this._buildMetadataEditor(); + } else { + this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight(); + this.layout({ metadataEditor: true }); + } + } else { + // we should collapse the metadata editor + this._metadataInfoContainer.style.display = 'none'; + this._metadataEditorDisposeStore.clear(); + this._layoutInfo.metadataHeight = 0; + this.layout({}); + } + } + + updateOutputRendering() { + if (this.cell.outputFoldingState === PropertyFoldingState.Expanded) { + this._outputInfoContainer.style.display = 'block'; + + if (!this._outputEditorContainer || !this._outputEditor) { + // create editor + this._outputEditorContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-editor-container')); + this._buildOutputEditor(); + } else { + this._layoutInfo.outputHeight = this._outputEditor.getContentHeight(); + this.layout({ outputEditor: true }); + } + } else { + this._outputInfoContainer.style.display = 'none'; + this._outputEditorDisposeStore.clear(); + this._layoutInfo.outputHeight = 0; + this.layout({}); + } + } + + protected _getFormatedMetadataJSON(metadata: NotebookCellMetadata, language?: string) { + const filteredMetadata: { [key: string]: any } = metadata; + const content = JSON.stringify({ + language, + ...filteredMetadata + }); + + const edits = format(content, undefined, {}); + const metadataSource = applyEdits(content, edits); + + return metadataSource; + } + + private _applySanitizedMetadataChanges(currentMetadata: NotebookCellMetadata, newMetadata: any) { + let result: { [key: string]: any } = {}; + let newLangauge: string | undefined = undefined; + try { + const newMetadataObj = JSON.parse(newMetadata); + const keys = new Set([...Object.keys(newMetadataObj)]); + for (let key of keys) { + switch (key as keyof NotebookCellMetadata) { + case 'breakpointMargin': + case 'editable': + case 'hasExecutionOrder': + case 'inputCollapsed': + case 'outputCollapsed': + case 'runnable': + // boolean + if (typeof newMetadataObj[key] === 'boolean') { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + + case 'executionOrder': + case 'lastRunDuration': + // number + if (typeof newMetadataObj[key] === 'number') { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + case 'runState': + // enum + if (typeof newMetadataObj[key] === 'number' && [1, 2, 3, 4].indexOf(newMetadataObj[key]) >= 0) { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + case 'statusMessage': + // string + if (typeof newMetadataObj[key] === 'string') { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + default: + if (key === 'language') { + newLangauge = newMetadataObj[key]; + } + result[key] = newMetadataObj[key]; + break; + } + } + + if (newLangauge !== undefined && newLangauge !== this.cell.modified!.language) { + this.notebookEditor.textModel!.changeCellLanguage(this.cell.modified!.handle, newLangauge); + } + this.notebookEditor.textModel!.changeCellMetadata(this.cell.modified!.handle, result, false); + } catch { + } + } + + private _buildMetadataEditor() { + if (this.cell.type === 'modified' || this.cell.type === 'unchanged') { + const originalMetadataSource = this._getFormatedMetadataJSON(this.cell.original?.metadata || {}, this.cell.original?.language); + const modifiedMetadataSource = this._getFormatedMetadataJSON(this.cell.modified?.metadata || {}, this.cell.modified?.language); + this._metadataEditor = this.instantiationService.createInstance(DiffEditorWidget, this._metadataEditorContainer!, { + ...fixedDiffEditorOptions, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: false, + originalEditable: false + }); + + DOM.addClass(this._metadataEditorContainer!, 'diff'); + + const mode = this.modeService.create('json'); + const originalMetadataModel = this.modelService.createModel(originalMetadataSource, mode, CellUri.generateCellMetadataUri(this.cell.original!.uri, this.cell.original!.handle), false); + const modifiedMetadataModel = this.modelService.createModel(modifiedMetadataSource, mode, CellUri.generateCellMetadataUri(this.cell.modified!.uri, this.cell.modified!.handle), false); + this._metadataEditor.setModel({ + original: originalMetadataModel, + modified: modifiedMetadataModel + }); + + this._register(originalMetadataModel); + this._register(modifiedMetadataModel); + + this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight(); + this.layout({ metadataEditor: true }); + + this._register(this._metadataEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { + this._layoutInfo.metadataHeight = e.contentHeight; + this.layout({ metadataEditor: true }); + } + })); + + let respondingToContentChange = false; + + this._register(modifiedMetadataModel.onDidChangeContent(() => { + respondingToContentChange = true; + const value = modifiedMetadataModel.getValue(); + this._applySanitizedMetadataChanges(this.cell.modified!.metadata, value); + this._metadataHeader.refresh(); + respondingToContentChange = false; + })); + + this._register(this.cell.modified!.onDidChangeMetadata(() => { + if (respondingToContentChange) { + return; + } + + const modifiedMetadataSource = this._getFormatedMetadataJSON(this.cell.modified?.metadata || {}, this.cell.modified?.language); + modifiedMetadataModel.setValue(modifiedMetadataSource); + })); + + return; + } + + this._metadataEditor = this.instantiationService.createInstance(CodeEditorWidget, this._metadataEditorContainer!, { + ...fixedEditorOptions, + dimension: { + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), + height: 0 + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: false + }, {}); + + const mode = this.modeService.create('jsonc'); + const originalMetadataSource = this._getFormatedMetadataJSON( + this.cell.type === 'insert' + ? this.cell.modified!.metadata || {} + : this.cell.original!.metadata || {}); + const uri = this.cell.type === 'insert' + ? this.cell.modified!.uri + : this.cell.original!.uri; + const handle = this.cell.type === 'insert' + ? this.cell.modified!.handle + : this.cell.original!.handle; + + const modelUri = CellUri.generateCellMetadataUri(uri, handle); + const metadataModel = this.modelService.createModel(originalMetadataSource, mode, modelUri, false); + this._metadataEditor.setModel(metadataModel); + this._register(metadataModel); + + this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight(); + this.layout({ metadataEditor: true }); + + this._register(this._metadataEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { + this._layoutInfo.metadataHeight = e.contentHeight; + this.layout({ metadataEditor: true }); + } + })); + } + + private _getFormatedOutputJSON(outputs: any[]) { + const content = JSON.stringify(outputs); + + const edits = format(content, undefined, {}); + const source = applyEdits(content, edits); + + return source; + } + + private _buildOutputEditor() { + if ((this.cell.type === 'modified' || this.cell.type === 'unchanged') && !this.notebookEditor.textModel!.transientOptions.transientOutputs) { + const originalOutputsSource = this._getFormatedOutputJSON(this.cell.original?.outputs || []); + const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []); + if (originalOutputsSource !== modifiedOutputsSource) { + this._outputEditor = this.instantiationService.createInstance(DiffEditorWidget, this._outputEditorContainer!, { + ...fixedDiffEditorOptions, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: true + }); + + DOM.addClass(this._outputEditorContainer!, 'diff'); + + const mode = this.modeService.create('json'); + const originalModel = this.modelService.createModel(originalOutputsSource, mode, undefined, true); + const modifiedModel = this.modelService.createModel(modifiedOutputsSource, mode, undefined, true); + this._outputEditor.setModel({ + original: originalModel, + modified: modifiedModel + }); + + this._layoutInfo.outputHeight = this._outputEditor.getContentHeight(); + this.layout({ outputEditor: true }); + + this._register(this._outputEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) { + this._layoutInfo.outputHeight = e.contentHeight; + this.layout({ outputEditor: true }); + } + })); + + this._register(this.cell.modified!.onDidChangeOutputs(() => { + const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []); + modifiedModel.setValue(modifiedOutputsSource); + this._outputHeader.refresh(); + })); + + return; + } + } + + this._outputEditor = this.instantiationService.createInstance(CodeEditorWidget, this._outputEditorContainer!, { + ...fixedEditorOptions, + dimension: { + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), + height: 0 + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() + }, {}); + + const mode = this.modeService.create('json'); + const originaloutputSource = this._getFormatedOutputJSON( + this.notebookEditor.textModel!.transientOptions + ? [] + : this.cell.type === 'insert' + ? this.cell.modified!.outputs || [] + : this.cell.original!.outputs || []); + const outputModel = this.modelService.createModel(originaloutputSource, mode, undefined, true); + this._outputEditor.setModel(outputModel); + + this._layoutInfo.outputHeight = this._outputEditor.getContentHeight(); + this.layout({ outputEditor: true }); + + this._register(this._outputEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) { + this._layoutInfo.outputHeight = e.contentHeight; + this.layout({ outputEditor: true }); + } + })); + } + + protected layoutNotebookCell() { + this.notebookEditor.layoutNotebookCell( + this.cell, + this._layoutInfo.editorHeight + + this._layoutInfo.editorMargin + + this._layoutInfo.metadataHeight + + this._layoutInfo.metadataStatusHeight + + this._layoutInfo.outputHeight + + this._layoutInfo.outputStatusHeight + + this._layoutInfo.bodyMargin + ); + } + + abstract initData(): void; + abstract styleContainer(container: HTMLElement): void; + abstract buildSourceEditor(sourceContainer: HTMLElement): void; + abstract onDidLayoutChange(event: CellDiffViewModelLayoutChangeEvent): void; + abstract layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }): void; +} + +export class DeletedCell extends AbstractCellRenderer { + private _editor!: CodeEditorWidget; + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: CellDiffViewModel, + readonly templateData: CellDiffRenderTemplate, + @IModeService readonly modeService: IModeService, + @IModelService readonly modelService: IModelService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + ) { + super(notebookEditor, cell, templateData, 'left', instantiationService, modeService, modelService); + } + + initData(): void { + } + + styleContainer(container: HTMLElement) { + DOM.addClass(container, 'removed'); + } + + buildSourceEditor(sourceContainer: HTMLElement): void { + const originalCell = this.cell.original!; + const lineCount = originalCell.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = lineCount * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; + + const editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); + + this._editor = this.instantiationService.createInstance(CodeEditorWidget, editorContainer, { + ...fixedEditorOptions, + dimension: { + width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, + height: editorHeight + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() + }, {}); + this._layoutInfo.editorHeight = editorHeight; + + this._register(this._editor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged) { + this._layoutInfo.editorHeight = e.contentHeight; + this.layout({ editorHeight: true }); + } + })); + + originalCell.resolveTextModelRef().then(ref => { + this._register(ref); + + const textModel = ref.object.textEditorModel; + this._editor.setModel(textModel); + this._layoutInfo.editorHeight = this._editor.getContentHeight(); + this.layout({ editorHeight: true }); + }); + + } + + onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) { + if (e.outerWidth !== undefined) { + this.layout({ outerWidth: true }); + } + } + layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) { + if (state.editorHeight || state.outerWidth) { + this._editor.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this._layoutInfo.editorHeight + }); + } + + if (state.metadataEditor || state.outerWidth) { + this._metadataEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this._layoutInfo.metadataHeight + }); + } + + if (state.outputEditor || state.outerWidth) { + this._outputEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this._layoutInfo.outputHeight + }); + } + + this.layoutNotebookCell(); + } +} + +export class InsertCell extends AbstractCellRenderer { + private _editor!: CodeEditorWidget; + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: CellDiffViewModel, + readonly templateData: CellDiffRenderTemplate, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IModeService readonly modeService: IModeService, + @IModelService readonly modelService: IModelService, + ) { + super(notebookEditor, cell, templateData, 'right', instantiationService, modeService, modelService); + } + + initData(): void { + } + + styleContainer(container: HTMLElement): void { + DOM.addClass(container, 'inserted'); + } + + buildSourceEditor(sourceContainer: HTMLElement): void { + const modifiedCell = this.cell.modified!; + const lineCount = modifiedCell.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = lineCount * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; + const editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); + + this._editor = this.instantiationService.createInstance(CodeEditorWidget, editorContainer, { + ...fixedEditorOptions, + dimension: { + width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, + height: editorHeight + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: false + }, {}); + + this._layoutInfo.editorHeight = editorHeight; + + this._register(this._editor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged) { + this._layoutInfo.editorHeight = e.contentHeight; + this.layout({ editorHeight: true }); + } + })); + + modifiedCell.resolveTextModelRef().then(ref => { + this._register(ref); + + const textModel = ref.object.textEditorModel; + this._editor.setModel(textModel); + this._layoutInfo.editorHeight = this._editor.getContentHeight(); + this.layout({ editorHeight: true }); + }); + } + + onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) { + if (e.outerWidth !== undefined) { + this.layout({ outerWidth: true }); + } + } + + layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) { + if (state.editorHeight || state.outerWidth) { + this._editor.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this._layoutInfo.editorHeight + }); + } + + if (state.metadataEditor || state.outerWidth) { + this._metadataEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), + height: this._layoutInfo.metadataHeight + }); + } + + if (state.outputEditor || state.outerWidth) { + this._outputEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), + height: this._layoutInfo.outputHeight + }); + } + + this.layoutNotebookCell(); + } +} + +export class ModifiedCell extends AbstractCellRenderer { + private _editor?: DiffEditorWidget; + private _editorContainer!: HTMLElement; + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: CellDiffViewModel, + readonly templateData: CellDiffRenderTemplate, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IModeService readonly modeService: IModeService, + @IModelService readonly modelService: IModelService, + ) { + super(notebookEditor, cell, templateData, 'full', instantiationService, modeService, modelService); + } + + initData(): void { + } + + styleContainer(container: HTMLElement): void { + } + + buildSourceEditor(sourceContainer: HTMLElement): void { + const modifiedCell = this.cell.modified!; + const lineCount = modifiedCell.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = lineCount * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; + this._editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); + + this._editor = this.instantiationService.createInstance(DiffEditorWidget, this._editorContainer, { + ...fixedDiffEditorOptions, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + originalEditable: false + }); + DOM.addClass(this._editorContainer, 'diff'); + + this._editor.layout({ + width: this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN, + height: editorHeight + }); + + this._editorContainer.style.height = `${editorHeight}px`; + + this._register(this._editor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged) { + this._layoutInfo.editorHeight = e.contentHeight; + this.layout({ editorHeight: true }); + } + })); + + this._initializeSourceDiffEditor(); + } + + private async _initializeSourceDiffEditor() { + const originalCell = this.cell.original!; + const modifiedCell = this.cell.modified!; + + const originalRef = await originalCell.resolveTextModelRef(); + const modifiedRef = await modifiedCell.resolveTextModelRef(); + const textModel = originalRef.object.textEditorModel; + const modifiedTextModel = modifiedRef.object.textEditorModel; + this._register(originalRef); + this._register(modifiedRef); + + this._editor!.setModel({ + original: textModel, + modified: modifiedTextModel + }); + + const contentHeight = this._editor!.getContentHeight(); + this._layoutInfo.editorHeight = contentHeight; + this.layout({ editorHeight: true }); + + } + + onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) { + if (e.outerWidth !== undefined) { + this.layout({ outerWidth: true }); + } + } + + layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) { + if (state.editorHeight || state.outerWidth) { + this._editorContainer.style.height = `${this._layoutInfo.editorHeight}px`; + this._editor!.layout(); + } + + if (state.metadataEditor || state.outerWidth) { + if (this._metadataEditorContainer) { + this._metadataEditorContainer.style.height = `${this._layoutInfo.metadataHeight}px`; + this._metadataEditor?.layout(); + } + } + + if (state.outputEditor || state.outerWidth) { + if (this._outputEditorContainer) { + this._outputEditorContainer.style.height = `${this._layoutInfo.outputHeight}px`; + this._outputEditor?.layout(); + } + } + + this.layoutNotebookCell(); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel.ts new file mode 100644 index 00000000000..55e25cee313 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { NotebookDiffEditorEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { CellDiffViewModelLayoutChangeEvent, DIFF_CELL_MARGIN } from 'vs/workbench/contrib/notebook/browser/diff/common'; +import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; + +export enum PropertyFoldingState { + Expanded, + Collapsed +} + +export class CellDiffViewModel extends Disposable { + public metadataFoldingState: PropertyFoldingState; + public outputFoldingState: PropertyFoldingState; + private _layoutInfoEmitter = new Emitter(); + + onDidLayoutChange = this._layoutInfoEmitter.event; + + constructor( + readonly original: NotebookCellTextModel | undefined, + readonly modified: NotebookCellTextModel | undefined, + readonly type: 'unchanged' | 'insert' | 'delete' | 'modified', + readonly editorEventDispatcher: NotebookDiffEditorEventDispatcher + ) { + super(); + this.metadataFoldingState = PropertyFoldingState.Collapsed; + this.outputFoldingState = PropertyFoldingState.Collapsed; + + this._register(this.editorEventDispatcher.onDidChangeLayout(e => { + this._layoutInfoEmitter.fire({ outerWidth: e.value.width }); + })); + } + + getComputedCellContainerWidth(layoutInfo: NotebookLayoutInfo, diffEditor: boolean, fullWidth: boolean) { + if (fullWidth) { + return layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0) - 2; + } + + return (layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)) / 2 - 18 - 2; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/common.ts b/src/vs/workbench/contrib/notebook/browser/diff/common.ts new file mode 100644 index 00000000000..505718cea7c --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/common.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 { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { Event } from 'vs/base/common/event'; +import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; + +export interface INotebookTextDiffEditor { + readonly textModel?: NotebookTextModel; + onMouseUp: Event<{ readonly event: MouseEvent; readonly target: CellDiffViewModel; }>; + getOverflowContainerDomNode(): HTMLElement; + getLayoutInfo(): NotebookLayoutInfo; + layoutNotebookCell(cell: CellDiffViewModel, height: number): void; +} + +export interface CellDiffRenderTemplate { + readonly container: HTMLElement; + readonly elementDisposables: DisposableStore; +} + +export interface CellDiffViewModelLayoutChangeEvent { + font?: BareFontInfo; + outerWidth?: number; +} + +export const DIFF_CELL_MARGIN = 16; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css new file mode 100644 index 00000000000..113ad130dae --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css @@ -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. + *--------------------------------------------------------------------------------------------*/ + +/* .notebook-diff-editor { + display: flex; + flex-direction: row; + height: 100%; + width: 100%; +} +.notebook-diff-editor-modified, +.notebook-diff-editor-original { + display: flex; + height: 100%; + width: 50%; +} */ + +.notebook-text-diff-editor .cell-body { + display: flex; + flex-direction: row; +} + +.notebook-text-diff-editor .cell-body.right { + flex-direction: row-reverse; +} + +.notebook-text-diff-editor .cell-body .diagonal-fill { + display: none; + width: 50%; +} + +.notebook-text-diff-editor .cell-body .cell-diff-editor-container { + width: 100%; + overflow: hidden; +} + +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .metadata-editor-container.diff, +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .output-editor-container.diff, +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container.diff { + /** 100% + diffOverviewWidth */ + width: calc(100% + 30px); +} + +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .metadata-editor-container .monaco-diff-editor .diffOverview, +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container.diff .monaco-diff-editor .diffOverview { + display: none; +} + +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .metadata-editor-container, +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container { + box-sizing: border-box; +} + +.notebook-text-diff-editor .cell-body.left .cell-diff-editor-container, +.notebook-text-diff-editor .cell-body.right .cell-diff-editor-container { + display: inline-block; + width: 50%; +} + +.notebook-text-diff-editor .cell-body.left .diagonal-fill, +.notebook-text-diff-editor .cell-body.right .diagonal-fill { + display: inline-block; + width: 50%; +} + +.notebook-text-diff-editor .cell-diff-editor-container .output-header-container, +.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container { + display: flex; + height: 24px; + align-items: center; + cursor: default; +} + +.notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-folding-indicator .codicon, +.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-folding-indicator .codicon { + visibility: visible; + padding: 4px 0 0 10px; + cursor: pointer; +} + +.notebook-text-diff-editor .cell-diff-editor-container .output-header-container, +.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container { + display: flex; + flex-direction: row; + align-items: center; +} + +.notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-toolbar, +.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-toolbar { + margin-left: auto; +} + +.notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-status, +.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-status { + font-size: 12px; +} + +.notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-status span, +.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-status span { + margin: 0 8px; + line-height: 21px; +} + +.notebook-text-diff-editor { + overflow: hidden; +} + +.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row, +.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover, +.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { + outline: none !important; +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts new file mode 100644 index 00000000000..856b0aae9bf --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.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 { localize } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor'; +import { ActiveEditorContext } from 'vs/workbench/common/editor'; +import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { NotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor'; +import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookDiffEditorInput'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +// ActiveEditorContext.isEqualTo(SearchEditorConstants.SearchEditorID) + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'notebook.diff.switchToText', + icon: { id: 'codicon/file-code' }, + title: { value: localize('notebook.diff.switchToText', "Open Text Diff Editor"), original: 'Open Text Diff Editor' }, + precondition: ActiveEditorContext.isEqualTo(NotebookTextDiffEditor.ID), + menu: [{ + id: MenuId.EditorTitle, + group: 'navigation', + when: ActiveEditorContext.isEqualTo(NotebookTextDiffEditor.ID) + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const editorGroupService = accessor.get(IEditorGroupsService); + + const activeEditor = editorService.activeEditorPane; + if (activeEditor && activeEditor instanceof NotebookTextDiffEditor) { + const leftResource = (activeEditor.input as NotebookDiffEditorInput).originalResource; + const rightResource = (activeEditor.input as NotebookDiffEditorInput).resource; + const options = { + preserveFocus: false + }; + + const label = localize('diffLeftRightLabel', "{0} āŸ· {1}", leftResource.toString(true), rightResource.toString(true)); + + await editorService.openEditor({ leftResource, rightResource, label, options }, viewColumnToEditorGroup(editorGroupService, undefined)); + } + } +}); + +registerAction2(class extends Action2 { + constructor() { + super( + { + id: 'notebook.diff.cell.revertMetadata', + title: localize('notebook.diff.cell.revertMetadata', "Revert Metadata"), + icon: { id: 'codicon/discard' }, + f1: false, + menu: { + id: MenuId.NotebookDiffCellMetadataTitle + } + } + ); + } + run(accessor: ServicesAccessor, context?: { cell: CellDiffViewModel }) { + if (!context) { + return; + } + + const original = context.cell.original; + const modified = context.cell.modified; + + if (!original || !modified) { + return; + } + + modified.metadata = original.metadata; + } +}); + +registerAction2(class extends Action2 { + constructor() { + super( + { + id: 'notebook.diff.cell.revertOutputs', + title: localize('notebook.diff.cell.revertOutputs', "Revert Outputs"), + icon: { id: 'codicon/discard' }, + f1: false, + menu: { + id: MenuId.NotebookDiffCellOutputsTitle + } + } + ); + } + run(accessor: ServicesAccessor, context?: { cell: CellDiffViewModel }) { + if (!context) { + return; + } + + const original = context.cell.original; + const modified = context.cell.modified; + + if (!original || !modified) { + return; + } + + modified.spliceNotebookCellOutputs([[0, modified.outputs.length, original.outputs]]); + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts new file mode 100644 index 00000000000..72e3d478988 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -0,0 +1,414 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import * as DOM from 'vs/base/browser/dom'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { notebookCellBorder, NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { NotebookDiffEditorInput } from '../notebookDiffEditorInput'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { WorkbenchList } from 'vs/platform/list/browser/listService'; +import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { CellDiffRenderer, NotebookCellTextDiffListDelegate, NotebookTextDiffList } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList'; +import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { diffDiagonalFill, diffInserted, diffRemoved, editorBackground, focusBorder, foreground } from 'vs/platform/theme/common/colorRegistry'; +import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { getZoomLevel } from 'vs/base/browser/browser'; +import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common'; +import { Emitter } from 'vs/base/common/event'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { NotebookDiffEditorEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { INotebookDiffEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +export const IN_NOTEBOOK_TEXT_DIFF_EDITOR = new RawContextKey('isInNotebookTextDiffEditor', false); + +export class NotebookTextDiffEditor extends EditorPane implements INotebookTextDiffEditor { + static readonly ID: string = 'workbench.editor.notebookTextDiffEditor'; + + private _rootElement!: HTMLElement; + private _overflowContainer!: HTMLElement; + private _dimension: DOM.Dimension | null = null; + private _list!: WorkbenchList; + private _fontInfo: BareFontInfo | undefined; + + private readonly _onMouseUp = this._register(new Emitter<{ readonly event: MouseEvent; readonly target: CellDiffViewModel; }>()); + public readonly onMouseUp = this._onMouseUp.event; + private _eventDispatcher: NotebookDiffEditorEventDispatcher | undefined; + protected _scopeContextKeyService!: IContextKeyService; + private _model: INotebookDiffEditorModel | null = null; + get textModel() { + return this._model?.modified.notebook; + } + + constructor( + @IInstantiationService readonly instantiationService: IInstantiationService, + @IThemeService readonly themeService: IThemeService, + @IContextKeyService readonly contextKeyService: IContextKeyService, + @INotebookEditorWorkerService readonly notebookEditorWorkerService: INotebookEditorWorkerService, + @IConfigurationService private readonly configurationService: IConfigurationService, + + @ITelemetryService telemetryService: ITelemetryService, + @IStorageService storageService: IStorageService, + ) { + super(NotebookTextDiffEditor.ID, telemetryService, themeService, storageService); + const editorOptions = this.configurationService.getValue('editor'); + this._fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()); + } + + protected createEditor(parent: HTMLElement): void { + this._rootElement = DOM.append(parent, DOM.$('.notebook-text-diff-editor')); + this._overflowContainer = document.createElement('div'); + DOM.addClass(this._overflowContainer, 'notebook-overflow-widget-container'); + DOM.addClass(this._overflowContainer, 'monaco-editor'); + DOM.append(parent, this._overflowContainer); + + const renderer = this.instantiationService.createInstance(CellDiffRenderer, this); + + this._list = this.instantiationService.createInstance( + NotebookTextDiffList, + 'NotebookTextDiff', + this._rootElement, + this.instantiationService.createInstance(NotebookCellTextDiffListDelegate), + [ + renderer + ], + this.contextKeyService, + { + setRowLineHeight: false, + setRowHeight: false, + supportDynamicHeights: true, + horizontalScrolling: false, + keyboardSupport: false, + mouseSupport: true, + multipleSelectionSupport: false, + enableKeyboardNavigation: true, + additionalScrollHeight: 0, + // transformOptimization: (isMacintosh && isNative) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native', + styleController: (_suffix: string) => { return this._list!; }, + overrideStyles: { + listBackground: editorBackground, + listActiveSelectionBackground: editorBackground, + listActiveSelectionForeground: foreground, + listFocusAndSelectionBackground: editorBackground, + listFocusAndSelectionForeground: foreground, + listFocusBackground: editorBackground, + listFocusForeground: foreground, + listHoverForeground: foreground, + listHoverBackground: editorBackground, + listHoverOutline: focusBorder, + listFocusOutline: focusBorder, + listInactiveSelectionBackground: editorBackground, + listInactiveSelectionForeground: foreground, + listInactiveFocusBackground: editorBackground, + listInactiveFocusOutline: editorBackground, + }, + accessibilityProvider: { + getAriaLabel() { return null; }, + getWidgetAriaLabel() { + return nls.localize('notebookTreeAriaLabel', "Notebook Text Diff"); + } + }, + // focusNextPreviousDelegate: { + // onFocusNext: (applyFocusNext: () => void) => this._updateForCursorNavigationMode(applyFocusNext), + // onFocusPrevious: (applyFocusPrevious: () => void) => this._updateForCursorNavigationMode(applyFocusPrevious), + // } + } + ); + + this._register(this._list.onMouseUp(e => { + if (e.element) { + this._onMouseUp.fire({ event: e.browserEvent, target: e.element }); + } + })); + } + + async setInput(input: NotebookDiffEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); + + this._model = await input.resolve(); + if (this._model === null) { + return; + } + + this._eventDispatcher = new NotebookDiffEditorEventDispatcher(); + + const diffResult = await this.notebookEditorWorkerService.computeDiff(this._model.original.resource, this._model.modified.resource); + const cellChanges = diffResult.cellsDiff.changes; + + const cellDiffViewModels: CellDiffViewModel[] = []; + const originalModel = this._model.original.notebook; + const modifiedModel = this._model.modified.notebook; + let originalCellIndex = 0; + let modifiedCellIndex = 0; + + for (let i = 0; i < cellChanges.length; i++) { + const change = cellChanges[i]; + // common cells + + for (let j = 0; j < change.originalStart - originalCellIndex; j++) { + const originalCell = originalModel.cells[originalCellIndex + j]; + const modifiedCell = modifiedModel.cells[modifiedCellIndex + j]; + if (originalCell.getHashValue() === modifiedCell.getHashValue()) { + cellDiffViewModels.push(new CellDiffViewModel( + originalCell, + modifiedCell, + 'unchanged', + this._eventDispatcher! + )); + } else { + cellDiffViewModels.push(new CellDiffViewModel( + originalCell, + modifiedCell, + 'modified', + this._eventDispatcher! + )); + } + } + + // modified cells + const modifiedLen = Math.min(change.originalLength, change.modifiedLength); + + for (let j = 0; j < modifiedLen; j++) { + cellDiffViewModels.push(new CellDiffViewModel( + originalModel.cells[change.originalStart + j], + modifiedModel.cells[change.modifiedStart + j], + 'modified', + this._eventDispatcher! + )); + } + + for (let j = modifiedLen; j < change.originalLength; j++) { + // deletion + cellDiffViewModels.push(new CellDiffViewModel( + originalModel.cells[change.originalStart + j], + undefined, + 'delete', + this._eventDispatcher! + )); + } + + for (let j = modifiedLen; j < change.modifiedLength; j++) { + // insertion + cellDiffViewModels.push(new CellDiffViewModel( + undefined, + modifiedModel.cells[change.modifiedStart + j], + 'insert', + this._eventDispatcher! + )); + } + + originalCellIndex = change.originalStart + change.originalLength; + modifiedCellIndex = change.modifiedStart + change.modifiedLength; + } + + for (let i = originalCellIndex; i < originalModel.cells.length; i++) { + cellDiffViewModels.push(new CellDiffViewModel( + originalModel.cells[i], + undefined, + 'delete', + this._eventDispatcher! + )); + } + + for (let i = modifiedCellIndex; i < modifiedModel.cells.length; i++) { + cellDiffViewModels.push(new CellDiffViewModel( + undefined, + modifiedModel.cells[i], + 'insert', + this._eventDispatcher! + )); + } + + this._list.splice(0, this._list.length, cellDiffViewModels); + } + + private pendingLayouts = new WeakMap(); + + + layoutNotebookCell(cell: CellDiffViewModel, height: number) { + const relayout = (cell: CellDiffViewModel, height: number) => { + const viewIndex = this._list!.indexOf(cell); + + this._list?.updateElementHeight(viewIndex, height); + }; + + if (this.pendingLayouts.has(cell)) { + this.pendingLayouts.get(cell)!.dispose(); + } + + let r: () => void; + const layoutDisposable = DOM.scheduleAtNextAnimationFrame(() => { + this.pendingLayouts.delete(cell); + + relayout(cell, height); + r(); + }); + + this.pendingLayouts.set(cell, toDisposable(() => { + layoutDisposable.dispose(); + r(); + })); + + return new Promise(resolve => { r = resolve; }); + } + + getDomNode() { + return this._rootElement; + } + + getOverflowContainerDomNode(): HTMLElement { + return this._overflowContainer; + } + + getControl(): NotebookEditorWidget | undefined { + return undefined; + } + + setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + super.setEditorVisible(visible, group); + } + + focus() { + super.focus(); + } + + clearInput(): void { + super.clearInput(); + } + + getLayoutInfo(): NotebookLayoutInfo { + if (!this._list) { + throw new Error('Editor is not initalized successfully'); + } + + return { + width: this._dimension!.width, + height: this._dimension!.height, + fontInfo: this._fontInfo! + }; + } + + layout(dimension: DOM.Dimension): void { + this._rootElement.classList.toggle('mid-width', dimension.width < 1000 && dimension.width >= 600); + this._rootElement.classList.toggle('narrow-width', dimension.width < 600); + this._dimension = dimension; + this._rootElement.style.height = `${dimension.height}px`; + + this._list?.layout(this._dimension.height, this._dimension.width); + this._eventDispatcher?.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + } +} + +registerThemingParticipant((theme, collector) => { + const cellBorderColor = theme.getColor(notebookCellBorder); + if (cellBorderColor) { + collector.addRule(`.notebook-text-diff-editor .cell-body { border: 1px solid ${cellBorderColor};}`); + collector.addRule(`.notebook-text-diff-editor .cell-diff-editor-container .output-header-container, + .notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container { + border-top: 1px solid ${cellBorderColor}; + }`); + } + + const diffDiagonalFillColor = theme.getColor(diffDiagonalFill); + collector.addRule(` + .notebook-text-diff-editor .diagonal-fill { + background-image: linear-gradient( + -45deg, + ${diffDiagonalFillColor} 12.5%, + #0000 12.5%, #0000 50%, + ${diffDiagonalFillColor} 50%, ${diffDiagonalFillColor} 62.5%, + #0000 62.5%, #0000 100% + ); + background-size: 8px 8px; + } + `); + + const added = theme.getColor(diffInserted); + if (added) { + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container { background-color: ${added}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container .monaco-editor .margin, + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container .monaco-editor .monaco-editor-background { + background-color: ${added}; + } + ` + ); + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .metadata-editor-container { background-color: ${added}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .metadata-editor-container .monaco-editor .margin, + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .metadata-editor-container .monaco-editor .monaco-editor-background { + background-color: ${added}; + } + ` + ); + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .output-editor-container { background-color: ${added}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .output-editor-container .monaco-editor .margin, + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .output-editor-container .monaco-editor .monaco-editor-background { + background-color: ${added}; + } + ` + ); + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .metadata-header-container { background-color: ${added}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .output-header-container { background-color: ${added}; } + ` + ); + } + const removed = theme.getColor(diffRemoved); + if (added) { + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container { background-color: ${removed}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container .monaco-editor .margin, + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container .monaco-editor .monaco-editor-background { + background-color: ${removed}; + } + ` + ); + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .metadata-editor-container { background-color: ${removed}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .metadata-editor-container .monaco-editor .margin, + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .metadata-editor-container .monaco-editor .monaco-editor-background { + background-color: ${removed}; + } + ` + ); + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .output-editor-container { background-color: ${removed}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .output-editor-container .monaco-editor .margin, + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .output-editor-container .monaco-editor .monaco-editor-background { + background-color: ${removed}; + } + ` + ); + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .metadata-header-container { background-color: ${removed}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .output-header-container { background-color: ${removed}; } + ` + ); + } + + // const changed = theme.getColor(editorGutterModifiedBackground); + + // if (changed) { + // collector.addRule(` + // .notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container.modified { + // background-color: ${changed}; + // } + // `); + // } + + collector.addRule(`.notebook-text-diff-editor .cell-body { margin: ${DIFF_CELL_MARGIN}px; }`); +}); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts new file mode 100644 index 00000000000..2931710c259 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts @@ -0,0 +1,228 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./notebookDiff'; +import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import * as DOM from 'vs/base/browser/dom'; +import { IListStyles, IStyleController } from 'vs/base/browser/ui/list/listWidget'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +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 { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { CellDiffRenderTemplate, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common'; +import { isMacintosh } from 'vs/base/common/platform'; +import { DeletedCell, InsertCell, ModifiedCell } from 'vs/workbench/contrib/notebook/browser/diff/cellComponents'; + +export class NotebookCellTextDiffListDelegate implements IListVirtualDelegate { + // private readonly lineHeight: number; + + constructor( + @IConfigurationService readonly configurationService: IConfigurationService + ) { + // const editorOptions = this.configurationService.getValue('editor'); + // this.lineHeight = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()).lineHeight; + } + + getHeight(element: CellDiffViewModel): number { + return 100; + } + + hasDynamicHeight(element: CellDiffViewModel): boolean { + return false; + } + + getTemplateId(element: CellDiffViewModel): string { + return CellDiffRenderer.TEMPLATE_ID; + } +} +export class CellDiffRenderer implements IListRenderer { + static readonly TEMPLATE_ID = 'cell_diff'; + + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + @IInstantiationService protected readonly instantiationService: IInstantiationService + ) { } + + get templateId() { + return CellDiffRenderer.TEMPLATE_ID; + } + + renderTemplate(container: HTMLElement): CellDiffRenderTemplate { + return { + container, + elementDisposables: new DisposableStore() + }; + } + + renderElement(element: CellDiffViewModel, index: number, templateData: CellDiffRenderTemplate, height: number | undefined): void { + templateData.container.innerText = ''; + switch (element.type) { + case 'unchanged': + templateData.elementDisposables.add(this.instantiationService.createInstance(ModifiedCell, this.notebookEditor, element, templateData)); + return; + case 'delete': + templateData.elementDisposables.add(this.instantiationService.createInstance(DeletedCell, this.notebookEditor, element, templateData)); + return; + case 'insert': + templateData.elementDisposables.add(this.instantiationService.createInstance(InsertCell, this.notebookEditor, element, templateData)); + return; + case 'modified': + templateData.elementDisposables.add(this.instantiationService.createInstance(ModifiedCell, this.notebookEditor, element, templateData)); + return; + default: + break; + } + } + + disposeTemplate(templateData: CellDiffRenderTemplate): void { + templateData.container.innerText = ''; + } + + disposeElement(element: CellDiffViewModel, index: number, templateData: CellDiffRenderTemplate): void { + templateData.elementDisposables.clear(); + } +} + + +export class NotebookTextDiffList extends WorkbenchList implements IDisposable, IStyleController { + private styleElement?: HTMLStyleElement; + + constructor( + listUser: string, + container: HTMLElement, + delegate: IListVirtualDelegate, + renderers: IListRenderer[], + contextKeyService: IContextKeyService, + options: IWorkbenchListOptions, + @IListService listService: IListService, + @IThemeService themeService: IThemeService, + @IConfigurationService configurationService: IConfigurationService, + @IKeybindingService keybindingService: IKeybindingService) { + super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService); + } + + style(styles: IListStyles) { + const selectorSuffix = this.view.domId; + if (!this.styleElement) { + this.styleElement = DOM.createStyleSheet(this.view.domNode); + } + const suffix = selectorSuffix && `.${selectorSuffix}`; + const content: string[] = []; + + if (styles.listBackground) { + if (styles.listBackground.isOpaque()) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows { background: ${styles.listBackground}; }`); + } else if (!isMacintosh) { // subpixel AA doesn't exist in macOS + console.warn(`List with id '${selectorSuffix}' was styled with a non-opaque background color. This will break sub-pixel antialiasing.`); + } + } + + if (styles.listFocusBackground) { + content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { background-color: ${styles.listFocusBackground}; }`); + content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused:hover { background-color: ${styles.listFocusBackground}; }`); // overwrite :hover style in this case! + } + + if (styles.listFocusForeground) { + content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { color: ${styles.listFocusForeground}; }`); + } + + if (styles.listActiveSelectionBackground) { + content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { background-color: ${styles.listActiveSelectionBackground}; }`); + content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected:hover { background-color: ${styles.listActiveSelectionBackground}; }`); // overwrite :hover style in this case! + } + + if (styles.listActiveSelectionForeground) { + content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { color: ${styles.listActiveSelectionForeground}; }`); + } + + if (styles.listFocusAndSelectionBackground) { + content.push(` + .monaco-drag-image, + .monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected.focused { background-color: ${styles.listFocusAndSelectionBackground}; } + `); + } + + if (styles.listFocusAndSelectionForeground) { + content.push(` + .monaco-drag-image, + .monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected.focused { color: ${styles.listFocusAndSelectionForeground}; } + `); + } + + if (styles.listInactiveFocusBackground) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { background-color: ${styles.listInactiveFocusBackground}; }`); + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused:hover { background-color: ${styles.listInactiveFocusBackground}; }`); // overwrite :hover style in this case! + } + + if (styles.listInactiveSelectionBackground) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { background-color: ${styles.listInactiveSelectionBackground}; }`); + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected:hover { background-color: ${styles.listInactiveSelectionBackground}; }`); // overwrite :hover style in this case! + } + + if (styles.listInactiveSelectionForeground) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { color: ${styles.listInactiveSelectionForeground}; }`); + } + + if (styles.listHoverBackground) { + content.push(`.monaco-list${suffix}:not(.drop-target) > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover:not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`); + } + + if (styles.listHoverForeground) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover:not(.selected):not(.focused) { color: ${styles.listHoverForeground}; }`); + } + + if (styles.listSelectionOutline) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { outline: 1px dotted ${styles.listSelectionOutline}; outline-offset: -1px; }`); + } + + if (styles.listFocusOutline) { + content.push(` + .monaco-drag-image, + .monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; } + `); + } + + if (styles.listInactiveFocusOutline) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { outline: 1px dotted ${styles.listInactiveFocusOutline}; outline-offset: -1px; }`); + } + + if (styles.listHoverOutline) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover { outline: 1px dashed ${styles.listHoverOutline}; outline-offset: -1px; }`); + } + + if (styles.listDropBackground) { + content.push(` + .monaco-list${suffix}.drop-target, + .monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows.drop-target, + .monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-row.drop-target { background-color: ${styles.listDropBackground} !important; color: inherit !important; } + `); + } + + if (styles.listFilterWidgetBackground) { + content.push(`.monaco-list-type-filter { background-color: ${styles.listFilterWidgetBackground} }`); + } + + if (styles.listFilterWidgetOutline) { + content.push(`.monaco-list-type-filter { border: 1px solid ${styles.listFilterWidgetOutline}; }`); + } + + if (styles.listFilterWidgetNoMatchesOutline) { + content.push(`.monaco-list-type-filter.no-matches { border: 1px solid ${styles.listFilterWidgetNoMatchesOutline}; }`); + } + + if (styles.listMatchesShadow) { + content.push(`.monaco-list-type-filter { box-shadow: 1px 1px 1px ${styles.listMatchesShadow}; }`); + } + + const newStyles = content.join('\n'); + if (newStyles !== this.styleElement.innerHTML) { + this.styleElement.innerHTML = newStyles; + } + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts index c7b5c94f98d..f9b0f8bd14d 100644 --- a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts +++ b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts @@ -25,16 +25,18 @@ export interface INotebookEditorContribution { namespace NotebookRendererContribution { export const viewType = 'viewType'; + export const id = 'id'; export const displayName = 'displayName'; export const mimeTypes = 'mimeTypes'; export const entrypoint = 'entrypoint'; } export interface INotebookRendererContribution { - readonly [NotebookRendererContribution.viewType]: string; + readonly [NotebookRendererContribution.id]?: string; + readonly [NotebookRendererContribution.viewType]?: string; readonly [NotebookRendererContribution.displayName]: string; readonly [NotebookRendererContribution.mimeTypes]?: readonly string[]; - readonly [NotebookRendererContribution.entrypoint]?: string; + readonly [NotebookRendererContribution.entrypoint]: string; } const notebookProviderContribution: IJSONSchema = { @@ -94,17 +96,23 @@ const notebookProviderContribution: IJSONSchema = { const notebookRendererContribution: IJSONSchema = { description: nls.localize('contributes.notebook.renderer', 'Contributes notebook output renderer provider.'), type: 'array', - defaultSnippets: [{ body: [{ viewType: '', displayName: '', mimeTypes: [''] }] }], + defaultSnippets: [{ body: [{ id: '', displayName: '', mimeTypes: [''] }] }], items: { type: 'object', required: [ - NotebookRendererContribution.viewType, + NotebookRendererContribution.id, NotebookRendererContribution.displayName, NotebookRendererContribution.mimeTypes, + NotebookRendererContribution.entrypoint, ], properties: { + [NotebookRendererContribution.id]: { + type: 'string', + description: nls.localize('contributes.notebook.renderer.viewType', 'Unique identifier of the notebook output renderer.'), + }, [NotebookRendererContribution.viewType]: { type: 'string', + deprecationMessage: nls.localize('contributes.notebook.provider.viewType.deprecated', 'Rename `viewType` to `id`.'), description: nls.localize('contributes.notebook.renderer.viewType', 'Unique identifier of the notebook output renderer.'), }, [NotebookRendererContribution.displayName]: { diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 80e04e4ae67..f0e489a8675 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -55,12 +55,22 @@ width: 100%; } +.monaco-workbench .notebookOverlay > .cell-list-container > .notebook-gutter > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { + cursor: default; + overflow: visible !important; + width: 100%; +} + .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image { position: absolute; top: -500px; z-index: 1000; } +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .execution-count-label { + display: none; +} + .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-editor-container > div { padding: 12px 16px; } @@ -136,6 +146,7 @@ transform: translate3d(0px, 0px, 0px); cursor: auto; box-sizing: border-box; + z-index: 27; /* Over drag handle */ } .monaco-workbench .notebookOverlay .output p { @@ -152,7 +163,7 @@ .monaco-workbench .notebookOverlay .output > div.foreground > .output-inner-container { width: 100%; - padding: 8px; + padding: 4px 8px; box-sizing: border-box; } @@ -175,7 +186,6 @@ height: 16px; cursor: pointer; padding: 4px; - z-index: 27; } .monaco-workbench .notebookOverlay .output .error_message { @@ -234,6 +244,7 @@ position: relative; left: -23px; cursor: pointer; + z-index: 27; /* Over drag handle */ } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.collapsed .notebook-folding-indicator, @@ -290,15 +301,26 @@ .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { visibility: hidden; - display: inline-block; + display: inline-flex; position: absolute; height: 26px; - right: 44px; top: -12px; /* this lines up the bottom toolbar border with the current line when on line 01 */ z-index: 30; } +.monaco-workbench .notebookOverlay.cell-title-toolbar-right > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { + right: 44px; +} + +.monaco-workbench .notebookOverlay.cell-title-toolbar-left > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { + left: 76px; +} + +.monaco-workbench .notebookOverlay.cell-title-toolbar-hidden > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { + display: none; +} + .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar .action-item { width: 24px; height: 24px; @@ -322,12 +344,16 @@ } .monaco-workbench .notebookOverlay .cell-statusbar-container { - height: 21px; + height: 22px; font-size: 12px; display: flex; position: relative; } +.monaco-workbench .notebookOverlay.cell-statusbar-hidden .cell-statusbar-container { + display: none; +} + .monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-left { display: flex; flex-grow: 1; @@ -335,10 +361,30 @@ .monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-right { padding-right: 12px; + z-index: 26; + display: flex; +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-right .cell-contributed-items-right { + display: flex; + flex-wrap: wrap; + overflow: hidden; +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-item { + display: flex; + align-items: center; + white-space: pre; + + height: 21px; /* Editor outline is -1px in, don't overlap */ + padding: 0px 6px; +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-item.cell-status-item-has-command { + cursor: pointer; } .monaco-workbench .notebookOverlay .cell-statusbar-container .cell-language-picker { - padding: 0px 6px; cursor: pointer; } @@ -352,6 +398,10 @@ align-items: center; } +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-message { + margin-right: 6px; +} + .monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status { height: 100%; display: flex; @@ -373,51 +423,42 @@ bottom: 0px; top: 0px; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container { - position: relative; +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container { + display: flex; + align-items: center; + justify-content: flex-end; + position: absolute; + top: 17px; height: 16px; - flex-shrink: 0; - top: 9px; +} + +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container .monaco-toolbar { + visibility: hidden; z-index: 27; /* Above the drag handle */ } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar { - visibility: hidden; -} - -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar .codicon { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container .monaco-toolbar .codicon { margin: 0; padding-right: 4px; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar .actions-container { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container .monaco-toolbar .actions-container { justify-content: center; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell.runnable .run-button-container .monaco-toolbar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell.runnable .run-button-container .monaco-toolbar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .cell.runnable .run-button-container .monaco-toolbar { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .runnable .run-button-container .monaco-toolbar, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .runnable .run-button-container .monaco-toolbar, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .runnable .run-button-container .monaco-toolbar { visibility: visible; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .execution-count-label { - position: absolute; - top: -2px; +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .execution-count-label { font-size: 10px; font-family: var(--monaco-monospace-font); - visibility: visible; white-space: pre; - width: 100%; - text-align: center; - padding-right: 8px; - box-sizing: border-box; opacity: .6; -} - -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell .run-button-container .execution-count-label, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .cell .run-button-container .execution-count-label, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell .run-button-container .execution-count-label { - visibility: hidden; + padding-top: 1px; + margin-right: 1px; } .monaco-workbench .notebookOverlay .cell .cell-editor-part { @@ -437,7 +478,7 @@ height: 2px; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-has-toolbar-actions.focused .cell-title-toolbar, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell-has-toolbar-actions .cell-title-toolbar, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-has-toolbar-actions.cell-output-hover .cell-title-toolbar, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-has-toolbar-actions:hover .cell-title-toolbar { visibility: visible; @@ -513,9 +554,16 @@ opacity: 0.5 !important; } + +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container.emptyNotebook { + opacity: 1 !important; +} + +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { position: absolute; display: flex; + align-items: center; justify-content: center; z-index: 25; /* over the focus outline on the editor, below the title toolbar */ width: 100%; @@ -528,23 +576,29 @@ display: none; } +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container:focus-within, +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container:hover, .monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:focus-within, .monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:hover { opacity: 1; } +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar { margin: 0px 8px; } +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-item, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar .action-item { display: flex; } +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-item.active, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar .action-item.active { transform: none; } +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-label, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar .action-label { font-size: 12px; margin: 0px; @@ -552,19 +606,23 @@ padding: 0px 4px; } +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-label .codicon, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar .action-label .codicon { margin-right: 3px; } +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-action-bar, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-action-bar { display: flex; align-items: center; } +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .action-item:first-child, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .action-item:first-child { margin-right: 16px; } +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container span.codicon, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container span.codicon { text-align: center; font-size: 14px; @@ -709,16 +767,21 @@ } .monaco-workbench .notebookOverlay > .cell-list-container .notebook-folding-indicator { + height: 20px; + width: 20px; + position: absolute; - top: 0; - left: 0; - right: 0; - height: 100%; + top: 6px; + left: 8px; + display: flex; + justify-content: center; + align-items: center; + z-index: 26; } .monaco-workbench .notebookOverlay > .cell-list-container .notebook-folding-indicator .codicon { visibility: visible; - padding: 8px 0 0 10px; + height: 16px; } /** Theming */ diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 7a9953b2be8..828c1b46615 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -30,17 +30,28 @@ import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEd import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl'; -import { CellKind, CellUri, getCellUndoRedoComparisonKey, NotebookDocumentBackupData, NotebookEditorPriority } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellToolbarLocKey, CellUri, DisplayOrderKey, getCellUndoRedoComparisonKey, NotebookDocumentBackupData, NotebookEditorPriority, NotebookTextDiffEditorPreview, ShowCellStatusbarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CustomEditorsAssociations, customEditorsAssociationsSettingId } from 'vs/workbench/services/editor/common/editorOpenWith'; import { CustomEditorInfo } from 'vs/workbench/contrib/customEditor/common/customEditor'; -import { NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; -import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditor, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { INotebookEditorModelResolverService, NotebookModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; +import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookDiffEditorInput'; +import { NotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor'; +import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerService'; +import { NotebookEditorWorkerServiceImpl } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl'; +import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; +import { NotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl'; +import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { Event } from 'vs/base/common/event'; // Editor Contribution @@ -51,13 +62,16 @@ import 'vs/workbench/contrib/notebook/browser/contrib/format/formatting'; import 'vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider'; import 'vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider'; import 'vs/workbench/contrib/notebook/browser/contrib/status/editorStatus'; +// import 'vs/workbench/contrib/notebook/browser/contrib/scm/scm'; + +// Diff Editor Contribution +import 'vs/workbench/contrib/notebook/browser/diff/notebookDiffActions'; // Output renderers registration import 'vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform'; import 'vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform'; import 'vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform'; -import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; /*--------------------------------------------------------------------------------------------- */ @@ -72,6 +86,17 @@ Registry.as(EditorExtensions.Editors).registerEditor( ] ); +Registry.as(EditorExtensions.Editors).registerEditor( + EditorDescriptor.create( + NotebookTextDiffEditor, + NotebookTextDiffEditor.ID, + 'Notebook Diff Editor' + ), + [ + new SyncDescriptor(NotebookDiffEditorInput) + ] +); + class NotebookEditorFactory implements IEditorInputFactory { canSerialize(): boolean { return true; @@ -226,6 +251,10 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri return undefined; } + if (originalInput instanceof DiffEditorInput && this.configurationService.getValue(NotebookTextDiffEditorPreview)) { + return this._handleDiffEditorInput(originalInput, options, group); + } + if (!originalInput.resource) { return undefined; } @@ -301,6 +330,49 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri const notebookOptions = new NotebookEditorOptions({ ...options, cellOptions, override: false, index }); return { override: this.editorService.openEditor(notebookInput, notebookOptions, group) }; } + + private _handleDiffEditorInput(diffEditorInput: DiffEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup): IOpenEditorOverride | undefined { + const modifiedInput = diffEditorInput.modifiedInput; + const originalInput = diffEditorInput.originalInput; + const notebookUri = modifiedInput.resource; + const originalNotebookUri = originalInput.resource; + + if (!notebookUri || !originalNotebookUri) { + return undefined; + } + + const existingEditors = group.editors.filter(editor => editor.resource && isEqual(editor.resource, notebookUri) && !(editor instanceof NotebookEditorInput)); + + if (existingEditors.length) { + return undefined; + } + + const userAssociatedEditors = this.getUserAssociatedEditors(notebookUri); + const notebookEditor = userAssociatedEditors.filter(association => this.notebookService.getContributedNotebookProvider(association.viewType)); + + if (userAssociatedEditors.length && !notebookEditor.length) { + // user pick a non-notebook editor for this resource + return undefined; + } + + // user might pick a notebook editor + + const associatedEditors = distinct([ + ...this.getUserAssociatedNotebookEditors(notebookUri), + ...(this.getContributedEditors(notebookUri).filter(editor => editor.priority === NotebookEditorPriority.default)) + ], editor => editor.id); + + if (!associatedEditors.length) { + // there is no notebook editor contribution which is enabled by default + return undefined; + } + + const info = associatedEditors[0]; + + const notebookInput = NotebookDiffEditorInput.create(this.instantiationService, notebookUri, modifiedInput.getName(), originalNotebookUri, originalInput.getName(), info.id); + const notebookOptions = new NotebookEditorOptions({ ...options, override: false }); + return { override: this.editorService.openEditor(notebookInput, notebookOptions, group) }; + } } class CellContentProvider implements ITextModelContentProvider { @@ -372,12 +444,120 @@ class CellContentProvider implements ITextModelContentProvider { } } +class RegisterSchemasContribution extends Disposable implements IWorkbenchContribution { + constructor() { + super(); + this.registerMetadataSchemas(); + } + + private registerMetadataSchemas(): void { + const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); + const metadataSchema: IJSONSchema = { + properties: { + ['language']: { + type: 'string', + description: 'The language for the cell' + }, + ['editable']: { + type: 'boolean', + description: `Controls whether a cell's editor is editable/readonly` + }, + ['runnable']: { + type: 'boolean', + description: 'Controls if the cell is executable' + }, + ['breakpointMargin']: { + type: 'boolean', + description: 'Controls if the cell has a margin to support the breakpoint UI' + }, + ['hasExecutionOrder']: { + type: 'boolean', + description: 'Whether the execution order indicator will be displayed' + }, + ['executionOrder']: { + type: 'number', + description: 'The order in which this cell was executed' + }, + ['statusMessage']: { + type: 'string', + description: `A status message to be shown in the cell's status bar` + }, + ['runState']: { + type: 'integer', + description: `The cell's current run state` + }, + ['runStartTime']: { + type: 'number', + description: 'If the cell is running, the time at which the cell started running' + }, + ['lastRunDuration']: { + type: 'number', + description: `The total duration of the cell's last run` + }, + ['inputCollapsed']: { + type: 'boolean', + description: `Whether a code cell's editor is collapsed` + }, + ['outputCollapsed']: { + type: 'boolean', + description: `Whether a code cell's outputs are collapsed` + } + }, + // patternProperties: allSettings.patternProperties, + additionalProperties: true, + allowTrailingCommas: true, + allowComments: true + }; + + jsonRegistry.registerSchema('vscode://schemas/notebook/cellmetadata', metadataSchema); + } +} + +// makes sure that every dirty notebook gets an editor +class NotebookFileTracker implements IWorkbenchContribution { + + private readonly _dirtyListener: IDisposable; + + constructor( + @INotebookService private readonly _notebookService: INotebookService, + @IEditorService private readonly _editorService: IEditorService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + ) { + this._dirtyListener = Event.debounce(workingCopyService.onDidChangeDirty, () => { }, 100)(() => { + const inputs = this._createMissingNotebookEditors(); + this._editorService.openEditors(inputs); + }); + } + + dispose(): void { + this._dirtyListener.dispose(); + } + + private _createMissingNotebookEditors(): IResourceEditorInput[] { + const result: IResourceEditorInput[] = []; + + for (const notebook of this._notebookService.getNotebookTextModels()) { + if (notebook.isDirty && !this._editorService.isOpen({ resource: notebook.uri })) { + result.push({ + resource: notebook.uri, + options: { inactive: true, preserveFocus: true, pinned: true } + }); + } + } + return result; + } +} + const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(NotebookContribution, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(CellContentProvider, LifecyclePhase.Starting); +workbenchContributionsRegistry.registerWorkbenchContribution(RegisterSchemasContribution, LifecyclePhase.Starting); +workbenchContributionsRegistry.registerWorkbenchContribution(NotebookFileTracker, LifecyclePhase.Ready); registerSingleton(INotebookService, NotebookService); +registerSingleton(INotebookEditorWorkerService, NotebookEditorWorkerServiceImpl); registerSingleton(INotebookEditorModelResolverService, NotebookModelResolverService, true); +registerSingleton(INotebookCellStatusBarService, NotebookCellStatusBarService, true); const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration({ @@ -386,13 +566,29 @@ configurationRegistry.registerConfiguration({ title: nls.localize('notebookConfigurationTitle', "Notebook"), type: 'object', properties: { - 'notebook.displayOrder': { - markdownDescription: nls.localize('notebook.displayOrder.description', "Priority list for output mime types"), + [DisplayOrderKey]: { + description: nls.localize('notebook.displayOrder.description', "Priority list for output mime types"), type: ['array'], items: { type: 'string' }, default: [] + }, + [CellToolbarLocKey]: { + description: nls.localize('notebook.cellToolbarLocation.description', "Where the cell toolbar should be shown, or whether it should be hidden."), + type: 'string', + enum: ['left', 'right', 'hidden'], + default: 'right' + }, + [ShowCellStatusbarKey]: { + description: nls.localize('notebook.showCellStatusbar.description', "Whether the cell statusbar should be shown."), + type: 'boolean', + default: true + }, + [NotebookTextDiffEditorPreview]: { + description: nls.localize('notebook.diff.enablePreview.description', "Whether to use the enhanced text diff editor for notebook."), + type: 'boolean', + default: true } } }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 58fdb4b4a8e..889546ea69e 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; -import { IListEvent, IListMouseEvent } from 'vs/base/browser/ui/list/list'; +import { IListContextMenuEvent, IListEvent, IListMouseEvent } from 'vs/base/browser/ui/list/list'; import { IListOptions, IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; @@ -19,14 +19,18 @@ import { Range } from 'vs/editor/common/core/range'; import { FindMatch, IReadonlyTextBuffer, ITextModel } from 'vs/editor/common/model'; import { ContextKeyExpr, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; -import { CellLanguageStatusBarItem, RunStateRenderer, TimerRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; +import { RunStateRenderer, TimerRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, IProcessedOutput, IRenderOutput, NotebookCellMetadata, NotebookDocumentMetadata, INotebookKernelInfo, IEditor, INotebookKernelInfo2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, IProcessedOutput, IRenderOutput, NotebookCellMetadata, NotebookDocumentMetadata, IEditor, INotebookKernelInfo2, IInsetRenderOutput, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { IMenu } from 'vs/platform/actions/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { EditorOptions } from 'vs/workbench/common/editor'; +import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation'; +import { CellEditorStatusBar } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets'; export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); @@ -45,6 +49,7 @@ export const NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK = new RawContextKey('no export const NOTEBOOK_VIEW_TYPE = new RawContextKey('notebookViewType', undefined); export const NOTEBOOK_CELL_TYPE = new RawContextKey('notebookCellType', undefined); // code, markdown export const NOTEBOOK_CELL_EDITABLE = new RawContextKey('notebookCellEditable', false); // bool +export const NOTEBOOK_CELL_FOCUSED = new RawContextKey('notebookCellFocused', false); // bool export const NOTEBOOK_CELL_RUNNABLE = new RawContextKey('notebookCellRunnable', false); // bool export const NOTEBOOK_CELL_MARKDOWN_EDIT_MODE = new RawContextKey('notebookCellMarkdownEditMode', false); // bool export const NOTEBOOK_CELL_RUN_STATE = new RawContextKey('notebookCellRunState', undefined); // idle, running @@ -160,6 +165,7 @@ export interface INotebookEditorContribution { export interface INotebookCellDecorationOptions { className?: string; + gutterClassName?: string; outputClassName?: string; } @@ -168,7 +174,35 @@ export interface INotebookDeltaDecoration { options: INotebookCellDecorationOptions; } +export class NotebookEditorOptions extends EditorOptions { + + readonly cellOptions?: IResourceEditorInput; + + constructor(options: Partial) { + super(); + this.overwrite(options); + this.cellOptions = options.cellOptions; + } + + with(options: Partial): NotebookEditorOptions { + return new NotebookEditorOptions({ ...this, ...options }); + } +} + +export type INotebookEditorContributionCtor = IConstructorSignature1; + +export interface INotebookEditorContributionDescription { + id: string; + ctor: INotebookEditorContributionCtor; +} + +export interface INotebookEditorCreationOptions { + readonly isEmbedded?: boolean; + readonly contributions?: INotebookEditorContributionDescription[]; +} + export interface INotebookEditor extends IEditor { + isEmbedded: boolean; cursorNavigationMode: boolean; @@ -183,11 +217,14 @@ export interface INotebookEditor extends IEditor { */ readonly onDidChangeModel: Event; readonly onDidFocusEditorWidget: Event; - isNotebookEditor: boolean; - activeKernel: INotebookKernelInfo | INotebookKernelInfo2 | undefined; + readonly isNotebookEditor: boolean; + activeKernel: INotebookKernelInfo2 | undefined; multipleKernelsAvailable: boolean; readonly onDidChangeAvailableKernels: Event; readonly onDidChangeKernel: Event; + readonly onDidChangeActiveCell: Event; + readonly onDidScroll: Event; + readonly onWillDispose: Event; isDisposed: boolean; @@ -205,6 +242,7 @@ export interface INotebookEditor extends IEditor { hasWebviewFocus(): boolean; hasOutputTextSelection(): boolean; + setOptions(options: NotebookEditorOptions | undefined): Promise; /** * Select & focus cell @@ -259,7 +297,7 @@ export interface INotebookEditor extends IEditor { /** * Move a cell to a specific position */ - moveCellToIdx(cell: ICellViewModel, index: number): Promise; + moveCellsToIdx(index: number, length: number, toIdx: number): Promise; /** * Focus the container of a cell (the monaco editor inside is not focused). @@ -299,7 +337,7 @@ export interface INotebookEditor extends IEditor { /** * Render the output in webview layer */ - createInset(cell: ICellViewModel, output: IProcessedOutput, shadowContent: string, offset: number): Promise; + createInset(cell: ICellViewModel, output: IInsetRenderOutput, offset: number): Promise; /** * Remove the output from the webview layer @@ -390,6 +428,8 @@ export interface INotebookEditor extends IEditor { setCellSelection(cell: ICellViewModel, selection: Range): void; + deltaCellDecorations(oldDecorations: string[], newDecorations: INotebookDeltaDecoration[]): string[]; + /** * Change the decorations on cells. * The notebook is virtualized and this method should be called to create/delete editor decorations safely. @@ -417,13 +457,16 @@ export interface INotebookEditor extends IEditor { } export interface INotebookCellList { - isDisposed: boolean + isDisposed: boolean; readonly contextKeyService: IContextKeyService; elementAt(position: number): ICellViewModel | undefined; elementHeight(element: ICellViewModel): number; onWillScroll: Event; + onDidScroll: Event; onDidChangeFocus: Event>; onDidChangeContentHeight: Event; + onDidChangeVisibleRanges: Event; + visibleRanges: ICellRange[]; scrollTop: number; scrollHeight: number; scrollLeft: number; @@ -433,6 +476,7 @@ export interface INotebookCellList { readonly onDidHideOutput: Event; readonly onMouseUp: Event>; readonly onMouseDown: Event>; + readonly onContextMenu: Event>; detachViewModel(): void; attachViewModel(viewModel: NotebookViewModel): void; clear(): void; @@ -475,15 +519,16 @@ export interface BaseCellRenderTemplate { contextKeyService: IContextKeyService; container: HTMLElement; cellContainer: HTMLElement; + decorationContainer: HTMLElement; toolbar: ToolBar; + deleteToolbar: ToolBar; betweenCellToolbar: ToolBar; focusIndicatorLeft: HTMLElement; disposables: DisposableStore; elementDisposables: DisposableStore; bottomCellContainer: HTMLElement; currentRenderedCell?: ICellViewModel; - statusBarContainer: HTMLElement; - languageStatusBarItem: CellLanguageStatusBarItem; + statusBar: CellEditorStatusBar; titleMenu: IMenu; toJSON: () => object; } @@ -496,7 +541,6 @@ export interface MarkdownCellRenderTemplate extends BaseCellRenderTemplate { export interface CodeCellRenderTemplate extends BaseCellRenderTemplate { cellRunState: RunStateRenderer; - cellStatusMessageContainer: HTMLElement; runToolbar: ToolBar; runButtonContainer: HTMLElement; executionOrderLabel: HTMLElement; @@ -520,7 +564,12 @@ export interface IOutputTransformContribution { */ dispose(): void; - render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput; + /** + * Returns contents to place in the webview inset, or the {@link IRenderNoOutput}. + * This call is allowed to have side effects, such as placing output + * directly into the container element. + */ + render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput; } export interface CellFindMatch { @@ -576,19 +625,20 @@ export interface CellViewModelStateChangeEvent { outputIsHoveredChanged?: boolean; } -/** - * [start, end] - */ -export interface ICellRange { - /** - * zero based index - */ - start: number; +export function cellRangesEqual(a: ICellRange[], b: ICellRange[]) { + a = reduceCellRanges(a); + b = reduceCellRanges(b); + if (a.length !== b.length) { + return false; + } - /** - * zero based index - */ - end: number; + for (let i = 0; i < a.length; i++) { + if (a[i].start !== b[i].start || a[i].end !== b[i].end) { + return false; + } + } + + return true; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts new file mode 100644 index 00000000000..27abf1c095a --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.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 { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; +import { URI } from 'vs/base/common/uri'; +import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; +import { INotebookCellStatusBarEntry } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +export class NotebookCellStatusBarService extends Disposable implements INotebookCellStatusBarService { + + private _onDidChangeEntriesForCell = new Emitter(); + readonly onDidChangeEntriesForCell: Event = this._onDidChangeEntriesForCell.event; + + private _entries = new ResourceMap>(); + + private removeEntry(entry: INotebookCellStatusBarEntry) { + const existingEntries = this._entries.get(entry.cellResource); + if (existingEntries) { + existingEntries.delete(entry); + if (!existingEntries.size) { + this._entries.delete(entry.cellResource); + } + } + + this._onDidChangeEntriesForCell.fire(entry.cellResource); + } + + addEntry(entry: INotebookCellStatusBarEntry): IDisposable { + const existingEntries = this._entries.get(entry.cellResource) ?? new Set(); + existingEntries.add(entry); + this._entries.set(entry.cellResource, existingEntries); + + this._onDidChangeEntriesForCell.fire(entry.cellResource); + + return { + dispose: () => { + this.removeEntry(entry); + } + }; + } + + getEntries(cell: URI): INotebookCellStatusBarEntry[] { + const existingEntries = this._entries.get(cell); + return existingEntries ? + Array.from(existingEntries.values()) : + []; + } + + readonly _serviceBrand: undefined; +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts new file mode 100644 index 00000000000..be382dcd70d --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts @@ -0,0 +1,224 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { EditorInput, IEditorInput, GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions, EditorModel } from 'vs/workbench/common/editor'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { URI } from 'vs/base/common/uri'; +import { isEqual } from 'vs/base/common/resources'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; +import { IReference } from 'vs/base/common/lifecycle'; +import { INotebookEditorModel, INotebookDiffEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; + +interface NotebookEditorInputOptions { + startDirty?: boolean; +} + +class NotebookDiffEditorModel extends EditorModel implements INotebookDiffEditorModel { + constructor( + readonly original: NotebookEditorModel, + readonly modified: NotebookEditorModel, + ) { + super(); + } + + async load(): Promise { + await this.original.load(); + await this.modified.load(); + + return this; + } + + async resolveOriginalFromDisk() { + await this.original.load({ forceReadFromDisk: true }); + } + + dispose(): void { + + } + +} + +export class NotebookDiffEditorInput extends EditorInput { + static create(instantiationService: IInstantiationService, resource: URI, name: string, originalResource: URI, originalName: string, viewType: string | undefined, options: NotebookEditorInputOptions = {}) { + return instantiationService.createInstance(NotebookDiffEditorInput, resource, name, originalResource, originalName, viewType, options); + } + + static readonly ID: string = 'workbench.input.diffNotebookInput'; + + private _textModel: IReference | null = null; + private _originalTextModel: IReference | null = null; + private _defaultDirtyState: boolean = false; + + constructor( + public readonly resource: URI, + public readonly name: string, + public readonly originalResource: URI, + public readonly originalName: string, + public readonly viewType: string | undefined, + public readonly options: NotebookEditorInputOptions, + @INotebookService private readonly _notebookService: INotebookService, + @INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService, + @IFilesConfigurationService private readonly _filesConfigurationService: IFilesConfigurationService, + @IFileDialogService private readonly _fileDialogService: IFileDialogService, + // @IInstantiationService private readonly _instantiationService: IInstantiationService + ) { + super(); + this._defaultDirtyState = !!options.startDirty; + } + + getTypeId(): string { + return NotebookDiffEditorInput.ID; + } + + getName(): string { + return nls.localize('sideBySideLabels', "{0} ā†” {1}", this.originalName, this.name); + } + + isDirty() { + if (!this._textModel) { + return !!this._defaultDirtyState; + } + return this._textModel.object.isDirty(); + } + + isUntitled(): boolean { + return this._textModel?.object.isUntitled() || false; + } + + isReadonly() { + return false; + } + + isSaving(): boolean { + if (this.isUntitled()) { + return false; // untitled is never saving automatically + } + + if (!this.isDirty()) { + return false; // the editor needs to be dirty for being saved + } + + if (this._filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { + return true; // a short auto save is configured, treat this as being saved + } + + return false; + } + + async save(group: GroupIdentifier, options?: ISaveOptions): Promise { + if (this._textModel) { + + if (this.isUntitled()) { + return this.saveAs(group, options); + } else { + await this._textModel.object.save(); + } + + return this; + } + + return undefined; + } + + async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { + if (!this._textModel || !this.viewType) { + return undefined; + } + + const provider = this._notebookService.getContributedNotebookProvider(this.viewType!); + + if (!provider) { + return undefined; + } + + const dialogPath = this._textModel.object.resource; + const target = await this._fileDialogService.pickFileToSave(dialogPath, options?.availableFileSystems); + if (!target) { + return undefined; // save cancelled + } + + if (!provider.matches(target)) { + const patterns = provider.selector.map(pattern => { + if (pattern.excludeFileNamePattern) { + return `${pattern.filenamePattern} (exclude: ${pattern.excludeFileNamePattern})`; + } + + return pattern.filenamePattern; + }).join(', '); + throw new Error(`File name ${target} is not supported by ${provider.providerDisplayName}. + +Please make sure the file name matches following patterns: +${patterns} +`); + } + + if (!await this._textModel.object.saveAs(target)) { + return undefined; + } + + return this._move(group, target)?.editor; + } + + // called when users rename a notebook document + rename(group: GroupIdentifier, target: URI): IMoveResult | undefined { + if (this._textModel) { + const contributedNotebookProviders = this._notebookService.getContributedNotebookProviders(target); + + if (contributedNotebookProviders.find(provider => provider.id === this._textModel!.object.viewType)) { + return this._move(group, target); + } + } + return undefined; + } + + private _move(group: GroupIdentifier, newResource: URI): { editor: IEditorInput } | undefined { + return undefined; + } + + async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { + if (this._textModel && this._textModel.object.isDirty()) { + await this._textModel.object.revert(options); + } + + return; + } + + async resolve(editorId?: string): Promise { + if (!await this._notebookService.canResolve(this.viewType!)) { + return null; + } + + if (!this._textModel) { + this._textModel = await this._notebookModelResolverService.resolve(this.resource, this.viewType!, editorId); + this._originalTextModel = await this._notebookModelResolverService.resolve(this.originalResource, this.viewType!, editorId); + } + + return new NotebookDiffEditorModel(this._originalTextModel!.object as NotebookEditorModel, this._textModel.object as NotebookEditorModel); + } + + matches(otherInput: unknown): boolean { + if (this === otherInput) { + return true; + } + if (otherInput instanceof NotebookDiffEditorInput) { + return this.viewType === otherInput.viewType + && isEqual(this.resource, otherInput.resource); + } + return false; + } + + dispose() { + if (this._textModel) { + this._textModel.dispose(); + this._textModel = null; + } + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index a954e866d9a..a60746ceea9 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -15,19 +15,20 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorOptions, IEditorInput, IEditorMemento } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { EditorOptions, IEditorInput, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; -import { NotebookEditorOptions, NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import { IBorrowValue, INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; import { INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { IEditorDropService } from 'vs/workbench/services/editor/browser/editorDropService'; import { IEditorGroup, IEditorGroupsService, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; -export class NotebookEditor extends BaseEditor { +export class NotebookEditor extends EditorPane { static readonly ID: string = 'workbench.editor.notebook'; private readonly _editorMemento: IEditorMemento; @@ -73,7 +74,7 @@ export class NotebookEditor extends BaseEditor { get minimumWidth(): number { return 375; } get maximumWidth(): number { return Number.POSITIVE_INFINITY; } - // these setters need to exist because this extends from BaseEditor + // these setters need to exist because this extends from EditorPane set minimumWidth(value: number) { /*noop*/ } set maximumWidth(value: number) { /*noop*/ } @@ -125,12 +126,12 @@ export class NotebookEditor extends BaseEditor { this._widget.value?.focus(); } - async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const group = this.group!; this._saveEditorViewState(this.input); - await super.setInput(input, options, token); + await super.setInput(input, options, context, token); // Check for cancellation if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorExtensions.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorExtensions.ts index 752a81728e1..01ea12b11a4 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorExtensions.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorExtensions.ts @@ -3,16 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrandedService, IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation'; -import { INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { BrandedService } from 'vs/platform/instantiation/common/instantiation'; +import { INotebookEditor, INotebookEditorContribution, INotebookEditorContributionCtor, INotebookEditorContributionDescription } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -export type INotebookEditorContributionCtor = IConstructorSignature1; - - -export interface INotebookEditorContributionDescription { - id: string; - ctor: INotebookEditorContributionCtor; -} class EditorContributionRegistry { public static readonly INSTANCE = new EditorContributionRegistry(); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 40d5380bd39..727dcb36e40 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -6,11 +6,17 @@ import { getZoomLevel } from 'vs/base/browser/browser'; import * as DOM from 'vs/base/browser/dom'; import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; +import { IAction, Separator } from 'vs/base/common/actions'; +import { SequencerByKey } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Color, RGBA } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { combinedDisposable, DisposableStore, Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { combinedDisposable, Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ScrollEvent } from 'vs/base/common/scrollable'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; import 'vs/css!./media/notebook'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; @@ -18,57 +24,43 @@ import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { Range } from 'vs/editor/common/core/range'; import { IEditor } from 'vs/editor/common/editorCommon'; import * as nls from 'vs/nls'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { contrastBorder, editorBackground, focusBorder, foreground, registerColor, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground, errorForeground, transparent, listFocusBackground, listInactiveSelectionBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, scrollbarSliderActiveBackground } from 'vs/platform/theme/common/colorRegistry'; +import { contrastBorder, diffInserted, diffRemoved, editorBackground, errorForeground, focusBorder, foreground, listFocusBackground, listInactiveSelectionBackground, registerColor, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground, transparent } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { EditorMemento } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorOptions, IEditorMemento } from 'vs/workbench/common/editor'; -import { CELL_MARGIN, CELL_RUN_GUTTER, CELL_TOP_MARGIN, SCROLLABLE_ELEMENT_PADDING_TOP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFocusMode, ICellRange, ICellViewModel, INotebookCellList, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_OUTPUT_FOCUSED, INotebookDeltaDecoration } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { EditorMemento } from 'vs/workbench/browser/parts/editor/editorPane'; +import { IEditorMemento } from 'vs/workbench/common/editor'; +import { Memento, MementoObject } from 'vs/workbench/common/memento'; +import { PANEL_BORDER } from 'vs/workbench/common/theme'; +import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugToolBar'; +import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_RUN_GUTTER, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CellEditState, CellFocusMode, ICellViewModel, INotebookCellList, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, NotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; +import { NotebookKernelProviderAssociations, notebookKernelProviderAssociationsSettingId } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; -import { CellDragAndDropController, CodeCellRenderer, MarkdownCellRenderer, NotebookCellListDelegate } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; +import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys'; +import { CodeCellRenderer, ListTopCellToolbar, MarkdownCellRenderer, NotebookCellListDelegate } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; +import { CellDragAndDropController } from 'vs/workbench/contrib/notebook/browser/view/renderers/dnd'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { CellViewModel, IModelDecorationsChangeAccessor, INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind, IProcessedOutput, INotebookKernelInfo, INotebookKernelInfoDto, INotebookKernelInfo2, NotebookRunState, NotebookCellRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { CellKind, CellToolbarLocKey, ICellRange, IInsetRenderOutput, INotebookKernelInfo2, IProcessedOutput, isTransformedDisplayOutput, NotebookCellRunState, NotebookRunState, ShowCellStatusbarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { editorGutterModifiedBackground } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; -import { generateUuid } from 'vs/base/common/uuid'; -import { Memento, MementoObject } from 'vs/workbench/common/memento'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { URI } from 'vs/base/common/uri'; -import { PANEL_BORDER } from 'vs/workbench/common/theme'; -import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugToolBar'; -import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys'; -import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; -import { notebookKernelProviderAssociationsSettingId, NotebookKernelProviderAssociations } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; const $ = DOM.$; -export class NotebookEditorOptions extends EditorOptions { - - readonly cellOptions?: IResourceEditorInput; - - constructor(options: Partial) { - super(); - this.overwrite(options); - this.cellOptions = options.cellOptions; - } - - with(options: Partial): NotebookEditorOptions { - return new NotebookEditorOptions({ ...this, ...options }); - } -} - const NotebookEditorActiveKernelCache = 'workbench.editor.notebook.activeKernel'; export class NotebookEditorWidget extends Disposable implements INotebookEditor { @@ -83,6 +75,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private _webviewTransparentCover: HTMLElement | null = null; private _list: INotebookCellList | undefined; private _dndController: CellDragAndDropController | null = null; + private _listTopCellToolbar: ListTopCellToolbar | null = null; private _renderedEditors: Map = new Map(); private _eventDispatcher: NotebookEventDispatcher | undefined; private _notebookViewModel: NotebookViewModel | undefined; @@ -104,6 +97,19 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private readonly _activeKernelMemento: Memento; private readonly _onDidFocusEmitter = this._register(new Emitter()); public readonly onDidFocus = this._onDidFocusEmitter.event; + private readonly _onWillScroll = this._register(new Emitter()); + public readonly onWillScroll: Event = this._onWillScroll.event; + private readonly _onWillDispose = this._register(new Emitter()); + public readonly onWillDispose: Event = this._onWillDispose.event; + + private readonly _insetModifyQueueByOutputId = new SequencerByKey(); + + set scrollTop(top: number) { + if (this._list) { + this._list.scrollTop = top; + } + } + private _cellContextKeyManager: CellContextKeyManager | null = null; private _isVisible = false; private readonly _uuid = generateUuid(); @@ -138,7 +144,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._notebookViewModel?.notebookDocument; } - private _activeKernel: INotebookKernelInfo | INotebookKernelInfo2 | undefined = undefined; + private _activeKernel: INotebookKernelInfo2 | undefined = undefined; private readonly _onDidChangeKernel = this._register(new Emitter()); readonly onDidChangeKernel: Event = this._onDidChangeKernel.event; private readonly _onDidChangeAvailableKernels = this._register(new Emitter()); @@ -148,7 +154,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._activeKernel; } - set activeKernel(kernel: INotebookKernelInfo | INotebookKernelInfo2 | undefined) { + set activeKernel(kernel: INotebookKernelInfo2 | undefined) { if (this._isDisposed) { return; } @@ -192,6 +198,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._renderedEditors.get(focused); } + private readonly _onDidChangeActiveCell = this._register(new Emitter()); + readonly onDidChangeActiveCell: Event = this._onDidChangeActiveCell.event; + + private readonly _onDidScroll = this._register(new Emitter()); + + readonly onDidScroll: Event = this._onDidScroll.event; + private _cursorNavigationMode: boolean = false; get cursorNavigationMode(): boolean { return this._cursorNavigationMode; @@ -201,15 +214,28 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._cursorNavigationMode = v; } + private readonly _onDidChangeVisibleRanges = this._register(new Emitter()); + onDidChangeVisibleRanges: Event = this._onDidChangeVisibleRanges.event; + + get visibleRanges() { + return this._list?.visibleRanges || []; + } + + readonly isEmbedded: boolean; + constructor( + readonly creationOptions: INotebookEditorCreationOptions, @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @INotebookService private notebookService: INotebookService, @IConfigurationService private readonly configurationService: IConfigurationService, @IContextKeyService readonly contextKeyService: IContextKeyService, - @ILayoutService private readonly layoutService: ILayoutService + @ILayoutService private readonly layoutService: ILayoutService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IMenuService private readonly menuService: IMenuService, ) { super(); + this.isEmbedded = creationOptions.isEmbedded || false; this._memento = new Memento(NotebookEditorWidget.ID, storageService); this._activeKernelMemento = new Memento(NotebookEditorActiveKernelCache, storageService); @@ -224,6 +250,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.layout(this._dimension); } } + + if (e.affectsConfiguration(CellToolbarLocKey) || e.affectsConfiguration(ShowCellStatusbarKey)) { + this._updateForNotebookConfiguration(); + } }); this.notebookService.addNotebookEditor(this); @@ -262,6 +292,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return true; } + private _updateForNotebookConfiguration() { + if (!this._overlayContainer) { + return; + } + + const cellToolbarLocation = this.configurationService.getValue(CellToolbarLocKey); + this._overlayContainer.classList.remove('cell-title-toolbar-left'); + this._overlayContainer.classList.remove('cell-title-toolbar-right'); + this._overlayContainer.classList.remove('cell-title-toolbar-hidden'); + + if (cellToolbarLocation === 'left' || cellToolbarLocation === 'right' || cellToolbarLocation === 'hidden') { + this._overlayContainer.classList.add(`cell-title-toolbar-${cellToolbarLocation}`); + } + + const showCellStatusBar = this.configurationService.getValue(ShowCellStatusbarKey); + this._overlayContainer.classList.toggle('cell-statusbar-hidden', !showCellStatusBar); + } + updateEditorFocus() { // Note - focus going to the webview will fire 'blur', but the webview element will be // a descendent of the notebook editor root. @@ -335,7 +383,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._notebookHasMultipleKernels = NOTEBOOK_HAS_MULTIPLE_KERNELS.bindTo(this.contextKeyService); this._notebookHasMultipleKernels.set(false); - const contributions = NotebookEditorExtensionsRegistry.getEditorContributions(); + let contributions: INotebookEditorContributionDescription[]; + if (Array.isArray(this.creationOptions.contributions)) { + contributions = this.creationOptions.contributions; + } else { + contributions = NotebookEditorExtensionsRegistry.getEditorContributions(); + } for (const desc of contributions) { try { @@ -345,6 +398,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor onUnexpectedError(err); } } + + this._updateForNotebookConfiguration(); } private _generateFontInfo(): void { @@ -377,6 +432,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._list = this.instantiationService.createInstance( NotebookCellList, 'NotebookCellList', + this._overlayContainer, this._body, this.instantiationService.createInstance(NotebookCellListDelegate), renderers, @@ -391,7 +447,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor multipleSelectionSupport: false, enableKeyboardNavigation: true, additionalScrollHeight: 0, - transformOptimization: true, + transformOptimization: false, //(isMacintosh && isNative) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native', styleController: (_suffix: string) => { return this._list!; }, overrideStyles: { listBackground: editorBackground, @@ -429,6 +485,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._register(this._list); this._register(combinedDisposable(...renderers)); + // top cell toolbar + this._listTopCellToolbar = this._register(this.instantiationService.createInstance(ListTopCellToolbar, this, this._list.rowsContainer)); + // transparent cover this._webviewTransparentCover = DOM.append(this._list.rowsContainer, $('.webview-cover')); this._webviewTransparentCover.style.display = 'none'; @@ -460,13 +519,46 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._register(this._list.onDidChangeFocus(_e => { this._onDidChangeActiveEditor.fire(this); + this._onDidChangeActiveCell.fire(); this._cursorNavigationMode = false; })); + this._register(this._list.onContextMenu(e => { + this.showListContextMenu(e); + })); + + this._register(this._list.onDidScroll((e) => { + this._onDidScroll.fire(e); + })); + + this._register(this._list.onDidChangeVisibleRanges(() => { + this._onDidChangeVisibleRanges.fire(); + })); + const widgetFocusTracker = DOM.trackFocus(this.getDomNode()); this._register(widgetFocusTracker); this._register(widgetFocusTracker.onDidFocus(() => this._onDidFocusEmitter.fire())); + } + private showListContextMenu(e: IListContextMenuEvent) { + this.contextMenuService.showContextMenu({ + getActions: () => { + const result: IAction[] = []; + const menu = this.menuService.createMenu(MenuId.NotebookCellTitle, this.contextKeyService); + const groups = menu.getActions(); + menu.dispose(); + + for (let group of groups) { + const [, actions] = group; + result.push(...actions); + result.push(new Separator()); + } + + result.pop(); // remove last separator + return result; + }, + getAnchor: () => e.anchor + }); } private _updateForCursorNavigationMode(applyFocusChange: () => void): void { @@ -546,7 +638,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor // we don't await for it, otherwise it will slow down the file opening this._setKernels(textModel, this._currentKernelTokenSource); - this._localStore.add(this.notebookService.onDidChangeKernels(async () => { + this._localStore.add(this.notebookService.onDidChangeKernels(async (e) => { + if (e && e.toString() !== this.textModel?.uri.toString()) { + // kernel update is not for current document. + return; + } this._currentKernelTokenSource?.cancel(); this._currentKernelTokenSource = new CancellationTokenSource(); await this._setKernels(textModel, this._currentKernelTokenSource); @@ -556,7 +652,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const focused = this._list!.getFocusedElements()[0]; if (focused) { if (!this._cellContextKeyManager) { - this._cellContextKeyManager = this._localStore.add(new CellContextKeyManager(this.contextKeyService, textModel, focused as CellViewModel)); + this._cellContextKeyManager = this._localStore.add(new CellContextKeyManager(this.contextKeyService, this, textModel, focused as CellViewModel)); } this._cellContextKeyManager.updateForElement(focused as CellViewModel); @@ -591,18 +687,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } } - } else if (this._notebookViewModel && this._notebookViewModel.viewCells.length === 1 && this._notebookViewModel.viewCells[0].cellKind === CellKind.Code) { - // there is only one code cell in the document - const cell = this._notebookViewModel!.viewCells[0]; - if (cell.getTextLength() === 0) { - // the cell is empty, very likely a template cell, focus it - this.selectElement(cell); - await this.revealLineInCenterAsync(cell, 1); - const editor = this._renderedEditors.get(cell)!; - if (editor) { - editor.focus(); - } - } } } @@ -628,16 +712,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } - const availableKernels = this.notebookService.getContributedNotebookKernels(textModel.viewType, textModel.uri); - if (tokenSource.token.isCancellationRequested) { return; } - if (provider.kernel && (availableKernels.length + availableKernels2.length) > 0) { - this._notebookHasMultipleKernels!.set(true); - this.multipleKernelsAvailable = true; - } else if ((availableKernels.length + availableKernels2.length) > 1) { + if ((availableKernels2.length) > 1) { this._notebookHasMultipleKernels!.set(true); this.multipleKernelsAvailable = true; } else { @@ -645,15 +724,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.multipleKernelsAvailable = false; } - // @deprecated - if (provider && provider.kernel) { - // it has a builtin kernel, don't automatically choose a kernel - await this._loadKernelPreloads(provider.providerExtensionLocation, provider.kernel); - tokenSource.dispose(); - return; - } - - const activeKernelStillExist = [...availableKernels2, ...availableKernels].find(kernel => kernel.id === this.activeKernel?.id && this.activeKernel?.id !== undefined); + const activeKernelStillExist = [...availableKernels2].find(kernel => kernel.id === this.activeKernel?.id && this.activeKernel?.id !== undefined); if (activeKernelStillExist) { // the kernel still exist, we don't want to modify the selection otherwise user's temporary preference is lost @@ -665,10 +736,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } // the provider doesn't have a builtin kernel, choose a kernel - this.activeKernel = availableKernels[0]; - if (this.activeKernel) { - await this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); - } + // this.activeKernel = availableKernels[0]; + // if (this.activeKernel) { + // await this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); + // } tokenSource.dispose(); } @@ -757,7 +828,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor tokenSource.dispose(); } - private async _loadKernelPreloads(extensionLocation: URI, kernel: INotebookKernelInfoDto) { + private async _loadKernelPreloads(extensionLocation: URI, kernel: INotebookKernelInfo2) { if (kernel.preloads && kernel.preloads.length) { await this._resolveWebview(); this._webview?.updateKernelPreloads([extensionLocation], kernel.preloads.map(preload => URI.revive(preload))); @@ -815,10 +886,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } })); - if (this.viewModel && this.viewModel!.renderers.size) { - this._webview?.updateRendererPreloads(this.viewModel!.renderers); - } - this._webviewResolved = true; resolve(this._webview!); @@ -864,12 +931,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - if (this.viewModel.renderers.size) { - await this._resolveWebview(); - this._webview?.updateRendererPreloads(this.viewModel.renderers); - } - this._localStore.add(this._list!.onWillScroll(e => { + this._onWillScroll.fire(e); if (!this._webviewResolved) { return; } @@ -1203,7 +1266,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const insertIndex = cell ? (direction === 'above' ? index : nextIndex) : index; - const newCell = this._notebookViewModel!.createCell(insertIndex, initialText.split(/\r?\n/g), language, type, undefined, true); + const focused = this._list?.getFocusedElements(); + const newCell = this._notebookViewModel!.createCell(insertIndex, initialText, language, type, undefined, true, undefined, focused); return newCell as CellViewModel; } @@ -1263,7 +1327,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } const newIdx = index + 2; // This is the adjustment for the index before the cell has been "removed" from its original index - return this._moveCellToIndex(index, newIdx); + return this._moveCellToIndex(index, 1, newIdx); } async moveCellUp(cell: ICellViewModel): Promise { @@ -1277,7 +1341,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } const newIdx = index - 1; - return this._moveCellToIndex(index, newIdx); + return this._moveCellToIndex(index, 1, newIdx); } async moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise { @@ -1293,34 +1357,33 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const relativeToIndex = this._notebookViewModel!.getCellIndex(relativeToCell); const newIdx = direction === 'above' ? relativeToIndex : relativeToIndex + 1; - return this._moveCellToIndex(originalIdx, newIdx); + return this._moveCellToIndex(originalIdx, 1, newIdx); } - async moveCellToIdx(cell: ICellViewModel, index: number): Promise { + async moveCellsToIdx(index: number, length: number, toIdx: number): Promise { if (!this._notebookViewModel!.metadata.editable) { return null; } - const originalIdx = this._notebookViewModel!.getCellIndex(cell); - return this._moveCellToIndex(originalIdx, index); + return this._moveCellToIndex(index, length, toIdx); } /** * @param index The current index of the cell - * @param newIdx The desired index, in an index scheme for the state of the tree before the current cell has been "removed". + * @param desiredIndex The desired index, in an index scheme for the state of the tree before the current cell has been "removed". * @example to move the cell from index 0 down one spot, call with (0, 2) */ - private async _moveCellToIndex(index: number, newIdx: number): Promise { - if (index < newIdx) { + private async _moveCellToIndex(index: number, length: number, desiredIndex: number): Promise { + if (index < desiredIndex) { // The cell is moving "down", it will free up one index spot and consume a new one - newIdx--; + desiredIndex -= length; } - if (index === newIdx) { + if (index === desiredIndex) { return null; } - if (!this._notebookViewModel!.moveCellToIdx(index, newIdx, true)) { + if (!this._notebookViewModel!.moveCellToIdx(index, length, desiredIndex, true)) { throw new Error('Notebook Editor move cell, index out of range'); } @@ -1330,7 +1393,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor r(null); } - const viewCell = this._notebookViewModel!.viewCells[newIdx]; + const viewCell = this._notebookViewModel!.viewCells[desiredIndex]; this._list?.revealElementInView(viewCell); r(viewCell); }); @@ -1368,15 +1431,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private async _cancelNotebookExecution(): Promise { const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; - if (provider) { - const viewType = provider.id; - const notebookUri = this._notebookViewModel!.uri; - - if (this._activeKernel) { - await (this._activeKernel as INotebookKernelInfo2).cancelNotebookCell!(this._notebookViewModel!.uri, undefined); - } else if (provider.kernel) { - return await this.notebookService.cancelNotebook(viewType, notebookUri); - } + if (provider && this._activeKernel) { + await (this._activeKernel as INotebookKernelInfo2).cancelNotebookCell!(this._notebookViewModel!.uri, undefined); } } @@ -1390,23 +1446,14 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private async _executeNotebook(): Promise { const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; - if (provider) { - const viewType = provider.id; - const notebookUri = this._notebookViewModel!.uri; - - if (this._activeKernel) { - // TODO@rebornix temp any cast, should be removed once we remove legacy kernel support - if ((this._activeKernel as INotebookKernelInfo2).executeNotebookCell) { - if (this._activeKernelResolvePromise) { - await this._activeKernelResolvePromise; - } - - await (this._activeKernel as INotebookKernelInfo2).executeNotebookCell!(this._notebookViewModel!.uri, undefined); - } else { - await this.notebookService.executeNotebook2(this._notebookViewModel!.viewType, this._notebookViewModel!.uri, this._activeKernel.id); + if (provider && this._activeKernel) { + // TODO@rebornix temp any cast, should be removed once we remove legacy kernel support + if ((this._activeKernel as INotebookKernelInfo2).executeNotebookCell) { + if (this._activeKernelResolvePromise) { + await this._activeKernelResolvePromise; } - } else if (provider.kernel) { - return await this.notebookService.executeNotebook(viewType, notebookUri); + + await (this._activeKernel as INotebookKernelInfo2).executeNotebookCell!(this._notebookViewModel!.uri, undefined); } } } @@ -1430,15 +1477,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private async _cancelNotebookCell(cell: ICellViewModel): Promise { const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; - if (provider) { - const viewType = provider.id; - const notebookUri = this._notebookViewModel!.uri; - - if (this._activeKernel) { - return await (this._activeKernel as INotebookKernelInfo2).cancelNotebookCell!(this._notebookViewModel!.uri, cell.handle); - } else if (provider.kernel) { - return await this.notebookService.cancelNotebookCell(viewType, notebookUri, cell.handle); - } + if (provider && this._activeKernel) { + return await (this._activeKernel as INotebookKernelInfo2).cancelNotebookCell!(this._notebookViewModel!.uri, cell.handle); } } @@ -1458,20 +1498,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private async _executeNotebookCell(cell: ICellViewModel): Promise { const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; if (provider) { - const viewType = provider.id; - const notebookUri = this._notebookViewModel!.uri; - if (this._activeKernel) { // TODO@rebornix temp any cast, should be removed once we remove legacy kernel support if ((this._activeKernel as INotebookKernelInfo2).executeNotebookCell) { await (this._activeKernel as INotebookKernelInfo2).executeNotebookCell!(this._notebookViewModel!.uri, cell.handle); - } else { - - return await this.notebookService.executeNotebookCell2(viewType, notebookUri, cell.handle, this._activeKernel.id); } - } else if (provider.kernel) { - return await this.notebookService.executeNotebookCell(viewType, notebookUri, cell.handle); } + } } @@ -1542,32 +1575,37 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._list?.triggerScrollFromMouseWheelEvent(event); } - async createInset(cell: CodeCellViewModel, output: IProcessedOutput, shadowContent: string, offset: number) { - if (!this._webview) { - return; - } + async createInset(cell: CodeCellViewModel, output: IInsetRenderOutput, offset: number): Promise { + this._insetModifyQueueByOutputId.queue(output.source.outputId, async () => { + if (!this._webview) { + return; + } - await this._resolveWebview(); + await this._resolveWebview(); - const preloads = this._notebookViewModel!.renderers; + if (!this._webview!.insetMapping.has(output.source)) { + const cellTop = this._list?.getAbsoluteTopOfElement(cell) || 0; + await this._webview!.createInset(cell, output, cellTop, offset); + } else { + const cellTop = this._list?.getAbsoluteTopOfElement(cell) || 0; + const scrollTop = this._list?.scrollTop || 0; - if (!this._webview!.insetMapping.has(output)) { - const cellTop = this._list?.getAbsoluteTopOfElement(cell) || 0; - await this._webview!.createInset(cell, output, cellTop, offset, shadowContent, preloads); - } else { - const cellTop = this._list?.getAbsoluteTopOfElement(cell) || 0; - const scrollTop = this._list?.scrollTop || 0; - - this._webview!.updateViewScrollTop(-scrollTop, true, [{ cell: cell, output: output, cellTop: cellTop }]); - } + this._webview!.updateViewScrollTop(-scrollTop, true, [{ cell, output: output.source, cellTop }]); + } + }); } removeInset(output: IProcessedOutput) { - if (!this._webview || !this._webviewResolved) { + if (!isTransformedDisplayOutput(output)) { return; } - this._webview!.removeInset(output); + this._insetModifyQueueByOutputId.queue(output.outputId, async () => { + if (!this._webview || !this._webviewResolved) { + return; + } + this._webview!.removeInset(output); + }); } hideInset(output: IProcessedOutput) { @@ -1575,7 +1613,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } - this._webview!.hideInset(output); + if (!isTransformedDisplayOutput(output)) { + return; + } + + this._insetModifyQueueByOutputId.queue(output.outputId, async () => { + this._webview!.hideInset(output); + }); } getOutputRenderer(): OutputRenderer { @@ -1618,6 +1662,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor dispose() { this._isDisposed = true; + this._onWillDispose.fire(); // dispose webview first this._webview?.dispose(); @@ -1630,6 +1675,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._localStore.clear(); this._list?.dispose(); + this._listTopCellToolbar?.dispose(); this._overlayContainer.remove(); this.viewModel?.dispose(); @@ -1745,19 +1791,12 @@ export const cellSymbolHighlight = registerColor('notebook.symbolHighlightBackgr }, nls.localize('notebook.symbolHighlightBackground', "Background color of highlighted cell")); registerThemingParticipant((theme, collector) => { - collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element { + collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element, + .notebookOverlay > .cell-list-container > .notebook-gutter > .monaco-list > .monaco-scrollable-element { padding-top: ${SCROLLABLE_ELEMENT_PADDING_TOP}px; box-sizing: border-box; }`); - // const color = getExtraColor(theme, embeddedEditorBackground, { dark: 'rgba(0, 0, 0, .4)', extra_dark: 'rgba(200, 235, 255, .064)', light: '#f4f4f4', hc: null }); - const color = theme.getColor(editorBackground); - if (color) { - collector.addRule(`.notebookOverlay .cell .monaco-editor-background, - .notebookOverlay .cell .margin-view-overlays, - .notebookOverlay .cell .cell-statusbar-container { background: ${color}; }`); - collector.addRule(`.notebookOverlay .cell-drag-image .cell-editor-container > div { background: ${color} !important; }`); - } const link = theme.getColor(textLinkForeground); if (link) { collector.addRule(`.notebookOverlay .output a, @@ -1794,16 +1833,22 @@ registerThemingParticipant((theme, collector) => { const editorBackgroundColor = theme.getColor(editorBackground); if (editorBackgroundColor) { - collector.addRule(`.notebookOverlay .cell-statusbar-container { border-top: solid 1px ${editorBackgroundColor}; }`); + collector.addRule(`.notebookOverlay .cell .monaco-editor-background, + .notebookOverlay .cell .margin-view-overlays, + .notebookOverlay .cell .cell-statusbar-container { background: ${editorBackgroundColor}; }`); + collector.addRule(`.notebookOverlay .cell-drag-image .cell-editor-container > div { background: ${editorBackgroundColor} !important; }`); + collector.addRule(`.notebookOverlay .monaco-list-row .cell-title-toolbar { background-color: ${editorBackgroundColor}; }`); collector.addRule(`.notebookOverlay .monaco-list-row.cell-drag-image { background-color: ${editorBackgroundColor}; }`); collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .action-item { background-color: ${editorBackgroundColor} }`); + collector.addRule(`.notebookOverlay .cell-list-top-cell-toolbar-container .action-item { background-color: ${editorBackgroundColor} }`); } const cellToolbarSeperator = theme.getColor(CELL_TOOLBAR_SEPERATOR); if (cellToolbarSeperator) { collector.addRule(`.notebookOverlay .monaco-list-row .cell-title-toolbar { border: solid 1px ${cellToolbarSeperator}; }`); collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .action-item { border: solid 1px ${cellToolbarSeperator} }`); + collector.addRule(`.notebookOverlay .cell-list-top-cell-toolbar-container .action-item { border: solid 1px ${cellToolbarSeperator} }`); collector.addRule(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-collapsed-part { border-bottom: solid 1px ${cellToolbarSeperator} }`); collector.addRule(`.notebookOverlay .monaco-action-bar .action-item.verticalSeparator { background-color: ${cellToolbarSeperator} }`); } @@ -1825,10 +1870,10 @@ registerThemingParticipant((theme, collector) => { } const focusedCellBorderColor = theme.getColor(focusedCellBorder); - collector.addRule(`.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-top:before, - .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-bottom:before, - .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row.focused:before, - .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row.focused:after { + collector.addRule(`.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-focus-indicator-top:before, + .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-focus-indicator-bottom:before, + .monaco-workbench .notebookOverlay .monaco-list:focus-within .markdown-cell-row.focused:before, + .monaco-workbench .notebookOverlay .monaco-list:focus-within .markdown-cell-row.focused:after { border-color: ${focusedCellBorderColor} !important; }`); @@ -1868,7 +1913,8 @@ registerThemingParticipant((theme, collector) => { const cellStatusBarHoverBg = theme.getColor(cellStatusBarItemHover); if (cellStatusBarHoverBg) { - collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-language-picker:hover { background-color: ${cellStatusBarHoverBg}; }`); + collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-language-picker:hover, + .monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-item.cell-status-item-has-command:hover { background-color: ${cellStatusBarHoverBg}; }`); } const cellInsertionIndicatorColor = theme.getColor(cellInsertionIndicator); @@ -1894,18 +1940,58 @@ registerThemingParticipant((theme, collector) => { collector.addRule(` .notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar > .slider.active:before { content: ""; width: 100%; height: 100%; position: absolute; background: ${scrollbarSliderActiveBackgroundColor}; } `); /* hack to not have cells see through scroller */ } + // case ChangeType.Modify: return theme.getColor(editorGutterModifiedBackground); + // case ChangeType.Add: return theme.getColor(editorGutterAddedBackground); + // case ChangeType.Delete: return theme.getColor(editorGutterDeletedBackground); + // diff + + const modifiedBackground = theme.getColor(editorGutterModifiedBackground); + if (modifiedBackground) { + collector.addRule(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row .nb-cell-modified .cell-focus-indicator { + background-color: ${modifiedBackground} !important; + } + + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .nb-cell-modified { + background-color: ${modifiedBackground} !important; + }`); + } + + const addedBackground = theme.getColor(diffInserted); + if (addedBackground) { + collector.addRule(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row .nb-cell-added .cell-focus-indicator { + background-color: ${addedBackground} !important; + } + + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .nb-cell-added { + background-color: ${addedBackground} !important; + }`); + } + const deletedBackground = theme.getColor(diffRemoved); + if (deletedBackground) { + collector.addRule(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row .nb-cell-deleted .cell-focus-indicator { + background-color: ${deletedBackground} !important; + } + + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .nb-cell-deleted { + background-color: ${deletedBackground} !important; + }`); + } + // Cell Margin collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell { margin: 0px ${CELL_MARGIN * 2}px 0px ${CELL_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell.code { margin-left: ${CODE_CELL_LEFT_MARGIN}px; }`); + collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell.code { margin-left: ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); + collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container { width: ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .cell-inner-container { padding-top: ${CELL_TOP_MARGIN}px; }`); collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .cell-inner-container { padding-bottom: ${CELL_BOTTOM_MARGIN}px; }`); collector.addRule(`.notebookOverlay .output { margin: 0px ${CELL_MARGIN}px 0px ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); collector.addRule(`.notebookOverlay .output { width: calc(100% - ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER + (CELL_MARGIN * 2)}px); }`); collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell.markdown { padding-left: ${CELL_RUN_GUTTER}px; }`); - collector.addRule(`.notebookOverlay .cell .run-button-container { width: 20px; margin: 0px ${Math.floor(CELL_RUN_GUTTER - 20) / 2}px; }`); collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top { height: ${CELL_TOP_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-side { bottom: ${BOTTOM_CELL_TOOLBAR_HEIGHT}px; }`); + collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-side { bottom: ${BOTTOM_CELL_TOOLBAR_GAP}px; }`); collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row.code-cell-row .cell-focus-indicator-left, .notebookOverlay .monaco-list .monaco-list-row.code-cell-row .cell-drag-handle { width: ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .cell-focus-indicator-left { width: ${CODE_CELL_LEFT_MARGIN}px; }`); @@ -1914,4 +2000,7 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-shadow-container-bottom { top: ${CELL_BOTTOM_MARGIN}px; }`); collector.addRule(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-collapsed-part { margin-left: ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; height: ${COLLAPSED_INDICATOR_HEIGHT}px; }`); + collector.addRule(`.notebookOverlay .cell-list-top-cell-toolbar-container { top: -${SCROLLABLE_ELEMENT_PADDING_TOP}px }`); + + collector.addRule(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { height: ${BOTTOM_CELL_TOOLBAR_HEIGHT}px }`); }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts index def8c4e957a..783b82070c7 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts @@ -126,7 +126,7 @@ class NotebookEditorWidgetService implements INotebookEditorWidgetService { if (!value) { // NEW widget const instantiationService = accessor.get(IInstantiationService); - const widget = instantiationService.createInstance(NotebookEditorWidget); + const widget = instantiationService.createInstance(NotebookEditorWidget, { isEmbedded: false }); widget.createEditor(); const token = this._tokenPool++; value = { widget, token }; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookPureOutputRenderer.ts b/src/vs/workbench/contrib/notebook/browser/notebookPureOutputRenderer.ts deleted file mode 100644 index c1f42f14d6c..00000000000 --- a/src/vs/workbench/contrib/notebook/browser/notebookPureOutputRenderer.ts +++ /dev/null @@ -1,49 +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, UriComponents } from 'vs/base/common/uri'; -import { INotebookRendererInfo, IOutputRenderResponse, IOutputRenderRequest } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { joinPath } from 'vs/base/common/resources'; - -/** - * A 'stub' output renderer used when the contribution has an `entrypoint` - * property. Include the entrypoint as its reload and renders an empty string. - */ -export class PureNotebookOutputRenderer implements INotebookRendererInfo { - - public readonly extensionId: ExtensionIdentifier; - public readonly extensionLocation: URI; - public readonly preloads: URI[]; - - - constructor(public readonly id: string, extension: IExtensionDescription, entrypoint: string) { - this.extensionId = extension.identifier; - this.extensionLocation = extension.extensionLocation; - this.preloads = [joinPath(extension.extensionLocation, entrypoint)]; - } - - public render(uri: URI, request: IOutputRenderRequest): Promise | undefined> { - return this.render2(uri, request); - } - - public render2(_uri: URI, request: IOutputRenderRequest): Promise | undefined> { - return Promise.resolve({ - items: request.items.map(cellInfo => ({ - key: cellInfo.key, - outputs: cellInfo.outputs.map(output => ({ - index: output.index, - outputId: output.outputId, - mimeType: output.mimeType, - handlerId: this.id, - // todo@connor4312: temp approach exploring this API: - transformedOutput: `` - })) - })) - }); - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 24821e8fbe3..ad28bde4973 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -3,43 +3,36 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import { Disposable, IDisposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { URI, UriComponents } from 'vs/base/common/uri'; -import { notebookProviderExtensionPoint, notebookRendererExtensionPoint, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/extensionPoint'; -import { NotebookProviderInfo, NotebookEditorDescriptor } from 'vs/workbench/contrib/notebook/common/notebookProvider'; -import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; -import { Emitter, Event } from 'vs/base/common/event'; -import { INotebookTextModel, INotebookRendererInfo, INotebookKernelInfo, CellOutputKind, ITransformedDisplayOutputDto, IDisplayOutput, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, NOTEBOOK_DISPLAY_ORDER, sortMimeTypes, IOrderedMimeType, mimeTypeSupportedByCore, IOutputRenderRequestOutputInfo, IOutputRenderRequestCellInfo, NotebookCellOutputsSplice, ICellEditOperation, CellEditType, ICellInsertEdit, IOutputRenderResponse, IProcessedOutput, BUILTIN_RENDERER_ID, NotebookEditorPriority, INotebookKernelProvider, notebookDocumentFilterMatch, INotebookKernelInfo2, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; -import { Iterable } from 'vs/base/common/iterator'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IEditorService, ICustomEditorViewTypesHandler, ICustomEditorInfo } from 'vs/workbench/services/editor/common/editorService'; -import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/common/notebookService'; -import * as glob from 'vs/base/common/glob'; -import { basename } from 'vs/base/common/path'; -import { getActiveNotebookEditor, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { Memento } from 'vs/workbench/common/memento'; -import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage'; -import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { generateUuid } from 'vs/base/common/uuid'; import { flatten } from 'vs/base/common/arrays'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { NotebookKernelProviderAssociationRegistry, updateNotebookKernelProvideAssociationSchema, NotebookViewTypesExtensionRegistry } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; -import { PureNotebookOutputRenderer } from 'vs/workbench/contrib/notebook/browser/notebookPureOutputRenderer'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Iterable } from 'vs/base/common/iterator'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; +import { URI } from 'vs/base/common/uri'; import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions'; import { CopyAction, CutAction, PasteAction } from 'vs/editor/contrib/clipboard/clipboard'; +import * as nls from 'vs/nls'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; +import { Memento } from 'vs/workbench/common/memento'; +import { INotebookEditorContribution, notebookProviderExtensionPoint, notebookRendererExtensionPoint } from 'vs/workbench/contrib/notebook/browser/extensionPoint'; +import { getActiveNotebookEditor, INotebookEditor, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookKernelProviderAssociationRegistry, NotebookViewTypesExtensionRegistry, updateNotebookKernelProvideAssociationSchema } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; - -function MODEL_ID(resource: URI): string { - return resource.toString(); -} +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, BUILTIN_RENDERER_ID, CellEditType, CellOutputKind, CellUri, DisplayOrderKey, ICellEditOperation, IDisplayOutput, INotebookKernelInfo2, INotebookKernelProvider, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellOutputsSplice, notebookDocumentFilterMatch, NotebookEditorPriority, NOTEBOOK_DISPLAY_ORDER, sortMimeTypes } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; +import { NotebookEditorDescriptor, NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; +import { IMainNotebookController, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { ICustomEditorInfo, ICustomEditorViewTypesHandler, IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; export class NotebookKernelProviderInfoStore extends Disposable { private readonly _notebookKernelProviders: INotebookKernelProvider[] = []; @@ -234,12 +227,10 @@ export class NotebookService extends Disposable implements INotebookService, ICu declare readonly _serviceBrand: undefined; static mainthreadNotebookDocumentHandle: number = 0; private readonly _notebookProviders = new Map(); - private readonly _notebookRenderers = new Map(); - private readonly _notebookKernels = new Map(); notebookProviderInfoStore: NotebookProviderInfoStore; notebookRenderersInfoStore: NotebookOutputRendererInfoStore = new NotebookOutputRendererInfoStore(); notebookKernelProviderInfoStore: NotebookKernelProviderInfoStore = new NotebookKernelProviderInfoStore(); - private readonly _models = new Map(); + private readonly _models = new ResourceMap(); private _onDidChangeActiveEditor = new Emitter(); onDidChangeActiveEditor: Event = this._onDidChangeActiveEditor.event; private _activeEditorDisposables = new DisposableStore(); @@ -260,8 +251,8 @@ export class NotebookService extends Disposable implements INotebookService, ICu private readonly _onDidChangeViewTypes = new Emitter(); onDidChangeViewTypes: Event = this._onDidChangeViewTypes.event; - private readonly _onDidChangeKernels = new Emitter(); - onDidChangeKernels: Event = this._onDidChangeKernels.event; + private readonly _onDidChangeKernels = new Emitter(); + onDidChangeKernels: Event = this._onDidChangeKernels.event; private readonly _onDidChangeNotebookActiveKernel = new Emitter<{ uri: URI, providerHandle: number | undefined, kernelId: string | undefined }>(); onDidChangeNotebookActiveKernel: Event<{ uri: URI, providerHandle: number | undefined, kernelId: string | undefined }> = this._onDidChangeNotebookActiveKernel.event; private cutItems: NotebookCellTextModel[] | undefined; @@ -291,25 +282,32 @@ export class NotebookService extends Disposable implements INotebookService, ICu for (const extension of renderers) { for (const notebookContribution of extension.value) { + if (!notebookContribution.entrypoint) { // avoid crashing + console.error(`Cannot register renderer for ${extension.description.identifier.value} since it did not have an entrypoint. This is now required: https://github.com/microsoft/vscode/issues/102644`); + continue; + } + + const id = notebookContribution.id ?? notebookContribution.viewType; + if (!id) { + console.error(`Notebook renderer from ${extension.description.identifier.value} is missing an 'id'`); + continue; + } + this.notebookRenderersInfoStore.add(new NotebookOutputRendererInfo({ - id: notebookContribution.viewType, + id, + extension: extension.description, + entrypoint: notebookContribution.entrypoint, displayName: notebookContribution.displayName, mimeTypes: notebookContribution.mimeTypes || [], })); - - if (notebookContribution.entrypoint) { - this._notebookRenderers.set(notebookContribution.viewType, new PureNotebookOutputRenderer(notebookContribution.viewType, extension.description, notebookContribution.entrypoint)); - } } } - - // console.log(this.notebookRenderersInfoStore); }); this._editorService.registerCustomEditorViewTypesHandler('Notebook', this); const updateOrder = () => { - const userOrder = this._configurationService.getValue('notebook.displayOrder'); + const userOrder = this._configurationService.getValue(DisplayOrderKey); this._displayOrder = { defaultOrder: this._accessibilityService.isScreenReaderOptimized() ? ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER : NOTEBOOK_DISPLAY_ORDER, userOrder: userOrder @@ -319,7 +317,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu updateOrder(); this._register(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectedKeys.indexOf('notebook.displayOrder') >= 0) { + if (e.affectedKeys.indexOf(DisplayOrderKey) >= 0) { updateOrder(); } })); @@ -342,7 +340,11 @@ export class NotebookService extends Disposable implements INotebookService, ICu this._register(UndoCommand.addImplementation(PRIORITY, () => { const { editor } = getContext(); if (editor?.viewModel) { - editor?.viewModel.undo(); + editor?.viewModel.undo().then(cellResources => { + if (cellResources?.length) { + editor?.setOptions(new NotebookEditorOptions({ cellOptions: { resource: cellResources[0] } })); + } + }); return true; } @@ -352,7 +354,11 @@ export class NotebookService extends Disposable implements INotebookService, ICu this._register(RedoCommand.addImplementation(PRIORITY, () => { const { editor } = getContext(); if (editor?.viewModel) { - editor?.viewModel.redo(); + editor?.viewModel.redo().then(cellResources => { + if (cellResources?.length) { + editor?.setOptions(new NotebookEditorOptions({ cellOptions: { resource: cellResources[0] } })); + } + }); return true; } @@ -361,6 +367,11 @@ export class NotebookService extends Disposable implements INotebookService, ICu if (CopyAction) { this._register(CopyAction.addImplementation(PRIORITY, accessor => { + const activeElement = document.activeElement; + if (activeElement && ['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) >= 0) { + return false; + } + const { editor, activeCell } = getContext(); if (!editor || !activeCell) { return false; @@ -382,6 +393,11 @@ export class NotebookService extends Disposable implements INotebookService, ICu if (PasteAction) { PasteAction.addImplementation(PRIORITY, () => { + const activeElement = document.activeElement; + if (activeElement && ['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) >= 0) { + return false; + } + const pasteCells = this.getToCopy(); if (!pasteCells) { @@ -389,7 +405,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu } const { editor, activeCell } = getContext(); - if (!editor || !activeCell) { + if (!editor) { return false; } @@ -403,38 +419,81 @@ export class NotebookService extends Disposable implements INotebookService, ICu return false; } - const currCellIndex = viewModel.getCellIndex(activeCell); + if (activeCell) { + const currCellIndex = viewModel.getCellIndex(activeCell); - let topPastedCell: CellViewModel | undefined = undefined; - pasteCells.items.reverse().map(cell => { - const data = CellUri.parse(cell.uri); + let topPastedCell: CellViewModel | undefined = undefined; + pasteCells.items.reverse().map(cell => { + const data = CellUri.parse(cell.uri); - if (pasteCells.isCopy || data?.notebook.toString() !== viewModel.uri.toString()) { - return viewModel.notebookDocument.createCellTextModel( - cell.getValue(), - cell.language, - cell.cellKind, - [], - cell.metadata - ); - } else { - return cell; + if (pasteCells.isCopy || data?.notebook.toString() !== viewModel.uri.toString()) { + return viewModel.notebookDocument.createCellTextModel( + cell.getValue(), + cell.language, + cell.cellKind, + [], + { + editable: cell.metadata?.editable, + runnable: cell.metadata?.runnable, + breakpointMargin: cell.metadata?.breakpointMargin, + hasExecutionOrder: cell.metadata?.hasExecutionOrder, + inputCollapsed: cell.metadata?.inputCollapsed, + outputCollapsed: cell.metadata?.outputCollapsed, + custom: cell.metadata?.custom + } + ); + } else { + return cell; + } + }).forEach(pasteCell => { + const newIdx = typeof currCellIndex === 'number' ? currCellIndex + 1 : 0; + topPastedCell = viewModel.insertCell(newIdx, pasteCell, true); + }); + + if (topPastedCell) { + editor.focusNotebookCell(topPastedCell, 'container'); + } + } else { + if (viewModel.length !== 0) { + return false; } - }).forEach(pasteCell => { - const newIdx = typeof currCellIndex === 'number' ? currCellIndex + 1 : 0; - topPastedCell = viewModel.insertCell(newIdx, pasteCell, true); - }); - if (topPastedCell) { - editor.focusNotebookCell(topPastedCell, 'container'); + let topPastedCell: CellViewModel | undefined = undefined; + pasteCells.items.reverse().map(cell => { + const data = CellUri.parse(cell.uri); + + if (pasteCells.isCopy || data?.notebook.toString() !== viewModel.uri.toString()) { + return viewModel.notebookDocument.createCellTextModel( + cell.getValue(), + cell.language, + cell.cellKind, + [], + cell.metadata + ); + } else { + return cell; + } + }).forEach(pasteCell => { + topPastedCell = viewModel.insertCell(0, pasteCell, true); + }); + + if (topPastedCell) { + editor.focusNotebookCell(topPastedCell, 'container'); + } } + return true; }); } if (CutAction) { CutAction.addImplementation(PRIORITY, accessor => { + const activeElement = document.activeElement; + if (activeElement && ['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) >= 0) { + return false; + } + const { editor, activeCell } = getContext(); if (!editor || !activeCell) { return false; @@ -476,6 +535,9 @@ export class NotebookService extends Disposable implements INotebookService, ICu // notebook providers/kernels/renderers might use `*` as activation event. await this._extensionService.activateByEvent(`*`); // this awaits full activation of all matching extensions + await this._extensionService.activateByEvent(`onNotebook:${viewType}`); + + // TODO@jrieken deprecated, remove this await this._extensionService.activateByEvent(`onNotebookEditor:${viewType}`); } return this._notebookProviders.has(viewType); @@ -483,7 +545,6 @@ export class NotebookService extends Disposable implements INotebookService, ICu registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: IMainNotebookController) { this._notebookProviders.set(viewType, { extensionData, controller }); - this.notebookProviderInfoStore.get(viewType)!.kernel = controller.kernel; this._onDidChangeViewTypes.fire(); } @@ -492,31 +553,13 @@ export class NotebookService extends Disposable implements INotebookService, ICu this._onDidChangeViewTypes.fire(); } - registerNotebookRenderer(id: string, renderer: INotebookRendererInfo) { - this._notebookRenderers.set(id, renderer); - } - - unregisterNotebookRenderer(id: string) { - this._notebookRenderers.delete(id); - } - - registerNotebookKernel(notebook: INotebookKernelInfo): void { - this._notebookKernels.set(notebook.id, notebook); - this._onDidChangeKernels.fire(); - } - - unregisterNotebookKernel(id: string): void { - this._notebookKernels.delete(id); - this._onDidChangeKernels.fire(); - } - registerNotebookKernelProvider(provider: INotebookKernelProvider): IDisposable { const d = this.notebookKernelProviderInfoStore.add(provider); - const kernelChangeEventListener = provider.onDidChangeKernels(() => { - this._onDidChangeKernels.fire(); + const kernelChangeEventListener = provider.onDidChangeKernels((e) => { + this._onDidChangeKernels.fire(e); }); - this._onDidChangeKernels.fire(); + this._onDidChangeKernels.fire(undefined); return toDisposable(() => { kernelChangeEventListener.dispose(); d.dispose(); @@ -536,6 +579,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu id: dto.id, label: dto.label, description: dto.description, + detail: dto.detail, isPreferred: dto.isPreferred, preloads: dto.preloads, providerHandle: dto.providerHandle, @@ -557,88 +601,41 @@ export class NotebookService extends Disposable implements INotebookService, ICu return flatten(result); } - getContributedNotebookKernels(viewType: string, resource: URI): INotebookKernelInfo[] { - let kernelInfos: INotebookKernelInfo[] = []; - this._notebookKernels.forEach(kernel => { - if (this._notebookKernelMatch(resource, kernel!.selectors)) { - kernelInfos.push(kernel!); - } - }); - - // sort by extensions - - const notebookContentProvider = this._notebookProviders.get(viewType); - - if (!notebookContentProvider) { - return kernelInfos; - } - - kernelInfos = kernelInfos.sort((a, b) => { - if (a.extension.value === notebookContentProvider!.extensionData.id.value) { - return -1; - } else if (b.extension.value === notebookContentProvider!.extensionData.id.value) { - return 1; - } else { - return 0; - } - }); - - return kernelInfos; - } - - private _notebookKernelMatch(resource: URI, selectors: (string | glob.IRelativePattern)[]): boolean { - for (let i = 0; i < selectors.length; i++) { - const pattern = typeof selectors[i] !== 'string' ? selectors[i] : selectors[i].toString(); - if (glob.match(pattern, basename(resource.fsPath).toLowerCase())) { - return true; - } - } - - return false; - } - getRendererInfo(id: string): INotebookRendererInfo | undefined { - const renderer = this._notebookRenderers.get(id); - - return renderer; + return this.notebookRenderersInfoStore.get(id); } - async resolveNotebook(viewType: string, uri: URI, forceReload: boolean, editorId?: string, backupId?: string): Promise { - const provider = this._notebookProviders.get(viewType); - if (!provider) { - return undefined; + async resolveNotebook(viewType: string, uri: URI, forceReload: boolean, editorId?: string, backupId?: string): Promise { + + if (!await this.canResolve(viewType)) { + throw new Error(`CANNOT load notebook, no provider for '${viewType}'`); } - const modelId = MODEL_ID(uri); - - let notebookModel: NotebookTextModel | undefined = undefined; - if (this._models.has(modelId)) { + const provider = this._notebookProviders.get(viewType)!; + let notebookModel: NotebookTextModel; + if (this._models.has(uri)) { // the model already exists - notebookModel = this._models.get(modelId)!.model; + notebookModel = this._models.get(uri)!.model; if (forceReload) { await provider.controller.reloadNotebook(notebookModel); } - return notebookModel; + } else { notebookModel = this._instantiationService.createInstance(NotebookTextModel, NotebookService.mainthreadNotebookDocumentHandle++, viewType, provider.controller.supportBackup, uri); await provider.controller.createNotebook(notebookModel, backupId); - - if (!notebookModel) { - return undefined; - } } // new notebook model created const modelData = new ModelData( - notebookModel!, + notebookModel, (model) => this._onWillDisposeDocument(model), ); - this._models.set(modelId, modelData); - this._onNotebookDocumentAdd.fire([notebookModel!.uri]); + this._models.set(uri, modelData); + this._onNotebookDocumentAdd.fire([notebookModel.uri]); // after the document is added to the store and sent to ext host, we transform the ouputs - await this.transformTextModelOutputs(notebookModel!); + await this.transformTextModelOutputs(notebookModel); if (editorId) { await provider.controller.resolveNotebookEditor(viewType, uri, editorId); @@ -648,60 +645,18 @@ export class NotebookService extends Disposable implements INotebookService, ICu } getNotebookTextModel(uri: URI): NotebookTextModel | undefined { - const modelId = MODEL_ID(uri); - - return this._models.get(modelId)?.model; + return this._models.get(uri)?.model; } - private async _fillInTransformedOutputs( - renderers: Set, - requestItems: IOutputRenderRequestCellInfo[], - renderFunc: (rendererId: string, items: IOutputRenderRequestCellInfo[]) => Promise | undefined>, - lookUp: (key: T) => { outputs: IProcessedOutput[] } - ) { - for (const id of renderers) { - const requestsPerRenderer: IOutputRenderRequestCellInfo[] = requestItems.map(req => { - return { - key: req.key, - outputs: req.outputs.filter(output => output.handlerId === id) - }; - }); - - const response = await renderFunc(id, requestsPerRenderer); - - // mix the response with existing outputs, which will replace the picked transformed mimetype with resolved result - if (response) { - response.items.forEach(cellInfo => { - const cell = lookUp(cellInfo.key)!; - cellInfo.outputs.forEach(outputInfo => { - const output = cell.outputs[outputInfo.index]; - if (output.outputKind === CellOutputKind.Rich && output.orderedMimeTypes && output.orderedMimeTypes.length) { - output.orderedMimeTypes[0] = { - mimeType: outputInfo.mimeType, - isResolved: true, - rendererId: outputInfo.handlerId, - output: outputInfo.transformedOutput - }; - } - }); - }); - } - } + getNotebookTextModels(): Iterable { + return Iterable.map(this._models.values(), data => data.model); } - async transformTextModelOutputs(textModel: NotebookTextModel) { - const renderers = new Set(); - - const cellMapping: Map = new Map(); - - const requestItems: IOutputRenderRequestCellInfo[] = []; + private async transformTextModelOutputs(textModel: NotebookTextModel) { for (let i = 0; i < textModel.cells.length; i++) { const cell = textModel.cells[i]; - cellMapping.set(cell.uri.fragment, cell); - const outputs = cell.outputs; - const outputRequest: IOutputRenderRequestOutputInfo[] = []; - outputs.forEach((output, index) => { + cell.outputs.forEach((output) => { if (output.outputKind === CellOutputKind.Rich) { // TODO no string[] casting const ret = this._transformMimeTypes(output, output.outputId, textModel.metadata.displayOrder as string[] || []); @@ -709,125 +664,53 @@ export class NotebookService extends Disposable implements INotebookService, ICu const pickedMimeTypeIndex = ret.pickedMimeTypeIndex!; output.pickedMimeTypeIndex = pickedMimeTypeIndex; output.orderedMimeTypes = orderedMimeTypes; - - if (orderedMimeTypes[pickedMimeTypeIndex!].rendererId && orderedMimeTypes[pickedMimeTypeIndex].rendererId !== BUILTIN_RENDERER_ID) { - outputRequest.push({ index, handlerId: orderedMimeTypes[pickedMimeTypeIndex].rendererId!, mimeType: orderedMimeTypes[pickedMimeTypeIndex].mimeType, outputId: output.outputId }); - renderers.add(orderedMimeTypes[pickedMimeTypeIndex].rendererId!); - } } }); - - requestItems.push({ key: cell.uri, outputs: outputRequest }); } - - await this._fillInTransformedOutputs(renderers, requestItems, async (rendererId, items) => { - return await this._notebookRenderers.get(rendererId)?.render(textModel.uri, { items: items }); - }, (key: UriComponents) => { return cellMapping.get(URI.revive(key).fragment)!; }); - - textModel.updateRenderers([...renderers]); } - async transformEditsOutputs(textModel: NotebookTextModel, edits: ICellEditOperation[]) { - const renderers = new Set(); - const requestItems: IOutputRenderRequestCellInfo<[number, number]>[] = []; - - edits.forEach((edit, editIndex) => { - if (edit.editType === CellEditType.Insert) { - edit.cells.forEach((cell, cellIndex) => { + transformEditsOutputs(textModel: NotebookTextModel, edits: ICellEditOperation[]) { + edits.forEach((edit) => { + if (edit.editType === CellEditType.Replace) { + edit.cells.forEach((cell) => { const outputs = cell.outputs; - const outputRequest: IOutputRenderRequestOutputInfo[] = []; - outputs.map((output, index) => { + outputs.map((output) => { if (output.outputKind === CellOutputKind.Rich) { const ret = this._transformMimeTypes(output, output.outputId, textModel.metadata.displayOrder as string[] || []); const orderedMimeTypes = ret.orderedMimeTypes!; const pickedMimeTypeIndex = ret.pickedMimeTypeIndex!; output.pickedMimeTypeIndex = pickedMimeTypeIndex; output.orderedMimeTypes = orderedMimeTypes; - - if (orderedMimeTypes[pickedMimeTypeIndex!].rendererId && orderedMimeTypes[pickedMimeTypeIndex].rendererId !== BUILTIN_RENDERER_ID) { - outputRequest.push({ index, handlerId: orderedMimeTypes[pickedMimeTypeIndex].rendererId!, mimeType: orderedMimeTypes[pickedMimeTypeIndex].mimeType, output: output, outputId: output.outputId }); - renderers.add(orderedMimeTypes[pickedMimeTypeIndex].rendererId!); - } } }); - - requestItems.push({ key: [editIndex, cellIndex], outputs: outputRequest }); + }); + } else if (edit.editType === CellEditType.Output) { + edit.outputs.map((output) => { + if (output.outputKind === CellOutputKind.Rich) { + const ret = this._transformMimeTypes(output, output.outputId, textModel.metadata.displayOrder as string[] || []); + const orderedMimeTypes = ret.orderedMimeTypes!; + const pickedMimeTypeIndex = ret.pickedMimeTypeIndex!; + output.pickedMimeTypeIndex = pickedMimeTypeIndex; + output.orderedMimeTypes = orderedMimeTypes; + } }); } }); - - await this._fillInTransformedOutputs<[number, number]>(renderers, requestItems, async (rendererId, items) => { - return await this._notebookRenderers.get(rendererId)?.render2<[number, number]>(textModel.uri, { items: items }); - }, (key: [number, number]) => { - return (edits[key[0]] as ICellInsertEdit).cells[key[1]]; - }); - - textModel.updateRenderers([...renderers]); } - async transformSpliceOutputs(textModel: NotebookTextModel, splices: NotebookCellOutputsSplice[]) { - const renderers = new Set(); - const requestItems: IOutputRenderRequestCellInfo[] = []; - - splices.forEach((splice, spliceIndex) => { + transformSpliceOutputs(textModel: NotebookTextModel, splices: NotebookCellOutputsSplice[]) { + splices.forEach((splice) => { const outputs = splice[2]; - const outputRequest: IOutputRenderRequestOutputInfo[] = []; - outputs.map((output, index) => { + outputs.map((output) => { if (output.outputKind === CellOutputKind.Rich) { const ret = this._transformMimeTypes(output, output.outputId, textModel.metadata.displayOrder as string[] || []); const orderedMimeTypes = ret.orderedMimeTypes!; const pickedMimeTypeIndex = ret.pickedMimeTypeIndex!; output.pickedMimeTypeIndex = pickedMimeTypeIndex; output.orderedMimeTypes = orderedMimeTypes; - - if (orderedMimeTypes[pickedMimeTypeIndex!].rendererId && orderedMimeTypes[pickedMimeTypeIndex].rendererId !== BUILTIN_RENDERER_ID) { - outputRequest.push({ index, handlerId: orderedMimeTypes[pickedMimeTypeIndex].rendererId!, mimeType: orderedMimeTypes[pickedMimeTypeIndex].mimeType, output: output, outputId: output.outputId }); - renderers.add(orderedMimeTypes[pickedMimeTypeIndex].rendererId!); - } } }); - requestItems.push({ key: spliceIndex, outputs: outputRequest }); }); - - await this._fillInTransformedOutputs(renderers, requestItems, async (rendererId, items) => { - return await this._notebookRenderers.get(rendererId)?.render2(textModel.uri, { items: items }); - }, (key: number) => { - return { outputs: splices[key][2] }; - }); - - textModel.updateRenderers([...renderers]); - } - - async transformSingleOutput(textModel: NotebookTextModel, output: IProcessedOutput, rendererId: string, mimeType: string): Promise { - const items = [ - { - key: 0, - outputs: [ - { - index: 0, - outputId: generateUuid(), - handlerId: rendererId, - mimeType: mimeType, - output: output - } - ] - } - ]; - const response = await this._notebookRenderers.get(rendererId)?.render2(textModel.uri, { items: items }); - - if (response) { - textModel.updateRenderers([rendererId]); - const outputInfo = response.items[0].outputs[0]; - - return { - mimeType: outputInfo.mimeType, - isResolved: true, - rendererId: outputInfo.handlerId, - output: outputInfo.transformedOutput - }; - } - - return; } private _transformMimeTypes(output: IDisplayOutput, outputId: string, documentDisplayOrder: string[]): ITransformedDisplayOutputDto { @@ -845,14 +728,12 @@ export class NotebookService extends Disposable implements INotebookService, ICu orderMimeTypes.push({ mimeType: mimeType, - isResolved: false, rendererId: handler.id, }); for (let i = 1; i < handlers.length; i++) { orderMimeTypes.push({ mimeType: mimeType, - isResolved: false, rendererId: handlers[i].id }); } @@ -860,14 +741,12 @@ export class NotebookService extends Disposable implements INotebookService, ICu if (mimeTypeSupportedByCore(mimeType)) { orderMimeTypes.push({ mimeType: mimeType, - isResolved: false, rendererId: BUILTIN_RENDERER_ID }); } } else { orderMimeTypes.push({ mimeType: mimeType, - isResolved: false, rendererId: BUILTIN_RENDERER_ID }); } @@ -886,54 +765,6 @@ export class NotebookService extends Disposable implements INotebookService, ICu return this.notebookRenderersInfoStore.getContributedRenderer(mimeType); } - async executeNotebook(viewType: string, uri: URI): Promise { - const provider = this._notebookProviders.get(viewType); - - if (provider) { - return provider.controller.executeNotebookByAttachedKernel(viewType, uri); - } - - return; - } - - async executeNotebookCell(viewType: string, uri: URI, handle: number): Promise { - const provider = this._notebookProviders.get(viewType); - if (provider) { - await provider.controller.executeNotebookCell(uri, handle); - } - } - - async cancelNotebook(viewType: string, uri: URI): Promise { - const provider = this._notebookProviders.get(viewType); - - if (provider) { - return provider.controller.cancelNotebookByAttachedKernel(viewType, uri); - } - - return; - } - - async cancelNotebookCell(viewType: string, uri: URI, handle: number): Promise { - const provider = this._notebookProviders.get(viewType); - if (provider) { - await provider.controller.cancelNotebookCell(uri, handle); - } - } - - async executeNotebook2(viewType: string, uri: URI, kernelId: string): Promise { - const kernel = this._notebookKernels.get(kernelId); - if (kernel) { - await kernel.executeNotebook(viewType, uri, undefined); - } - } - - async executeNotebookCell2(viewType: string, uri: URI, handle: number, kernelId: string): Promise { - const kernel = this._notebookKernels.get(kernelId); - if (kernel) { - await kernel.executeNotebook(viewType, uri, handle); - } - } - getContributedNotebookProviders(resource: URI): readonly NotebookProviderInfo[] { return this.notebookProviderInfoStore.getContributedNotebook(resource); } @@ -942,8 +773,8 @@ export class NotebookService extends Disposable implements INotebookService, ICu return this.notebookProviderInfoStore.get(viewType); } - getContributedNotebookOutputRenderers(mimeType: string): readonly NotebookOutputRendererInfo[] { - return this.notebookRenderersInfoStore.getContributedRenderer(mimeType); + getContributedNotebookOutputRenderers(viewType: string): NotebookOutputRendererInfo | undefined { + return this.notebookRenderersInfoStore.get(viewType); } getNotebookProviderResourceRoots(): URI[] { @@ -1075,10 +906,9 @@ export class NotebookService extends Disposable implements INotebookService, ICu } private _onWillDisposeDocument(model: INotebookTextModel): void { - const modelId = MODEL_ID(model.uri); - const modelData = this._models.get(modelId); - this._models.delete(modelId); + const modelData = this._models.get(model.uri); + this._models.delete(model.uri); if (modelData) { // delete editors and documents diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 687a666c773..f9846096655 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -19,9 +19,9 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { CellRevealPosition, CellRevealType, CursorAtBoundary, getVisibleCells, ICellRange, ICellViewModel, INotebookCellList, reduceCellRanges, CellEditState, CellFocusMode, BaseCellRenderTemplate, NOTEBOOK_CELL_LIST_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellRevealPosition, CellRevealType, CursorAtBoundary, getVisibleCells, ICellViewModel, INotebookCellList, reduceCellRanges, CellEditState, CellFocusMode, BaseCellRenderTemplate, NOTEBOOK_CELL_LIST_FOCUSED, cellRangesEqual } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { diff, IProcessedOutput, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { diff, IProcessedOutput, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { clamp } from 'vs/base/common/numbers'; import { SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; @@ -54,16 +54,37 @@ export class NotebookCellList extends WorkbenchList implements ID private _hiddenRangeIds: string[] = []; private hiddenRangesPrefixSum: PrefixSumComputer | null = null; + private readonly _onDidChangeVisibleRanges = new Emitter(); + + onDidChangeVisibleRanges: Event = this._onDidChangeVisibleRanges.event; + private _visibleRanges: ICellRange[] = []; + + get visibleRanges() { + return this._visibleRanges; + } + + set visibleRanges(ranges: ICellRange[]) { + if (cellRangesEqual(this._visibleRanges, ranges)) { + return; + } + + this._visibleRanges = ranges; + this._onDidChangeVisibleRanges.fire(); + } + private _isDisposed = false; get isDisposed() { return this._isDisposed; } + private _isInLayout: boolean = false; + private readonly _focusNextPreviousDelegate: IFocusNextPreviousDelegate; constructor( private listUser: string, + parentContainer: HTMLElement, container: HTMLElement, delegate: IListVirtualDelegate, renderers: IListRenderer[], @@ -151,6 +172,86 @@ export class NotebookCellList extends WorkbenchList implements ID focus.focusMode = CellFocusMode.Editor; } })); + + // update visibleRanges + const updateVisibleRanges = () => { + if (!this.view.length) { + return; + } + + const top = this.getViewScrollTop(); + const bottom = this.getViewScrollBottom(); + const topViewIndex = clamp(this.view.indexAt(top), 0, this.view.length - 1); + const topElement = this.view.element(topViewIndex); + const topModelIndex = this._viewModel!.getCellIndex(topElement); + const bottomViewIndex = clamp(this.view.indexAt(bottom), 0, this.view.length - 1); + const bottomElement = this.view.element(bottomViewIndex); + const bottomModelIndex = this._viewModel!.getCellIndex(bottomElement); + + if (bottomModelIndex - topModelIndex === bottomViewIndex - topViewIndex) { + this.visibleRanges = [{ start: topModelIndex, end: bottomModelIndex }]; + } else { + let stack: number[] = []; + const ranges: ICellRange[] = []; + // there are hidden ranges + let index = topViewIndex; + let modelIndex = topModelIndex; + + while (index <= bottomViewIndex) { + const accu = this.hiddenRangesPrefixSum!.getAccumulatedValue(index); + if (accu === modelIndex + 1) { + // no hidden area after it + if (stack.length) { + if (stack[stack.length - 1] === modelIndex - 1) { + ranges.push({ start: stack[stack.length - 1], end: modelIndex }); + } else { + ranges.push({ start: stack[stack.length - 1], end: stack[stack.length - 1] }); + } + } + + stack.push(modelIndex); + index++; + modelIndex++; + } else { + // there are hidden ranges after it + if (stack.length) { + if (stack[stack.length - 1] === modelIndex - 1) { + ranges.push({ start: stack[stack.length - 1], end: modelIndex }); + } else { + ranges.push({ start: stack[stack.length - 1], end: stack[stack.length - 1] }); + } + } + + stack.push(modelIndex); + index++; + modelIndex = accu; + } + } + + if (stack.length) { + ranges.push({ start: stack[stack.length - 1], end: stack[stack.length - 1] }); + } + + this.visibleRanges = reduceCellRanges(ranges); + } + }; + + this._localDisposableStore.add(this.view.onDidChangeContentHeight(() => { + if (this._isInLayout) { + DOM.scheduleAtNextAnimationFrame(() => { + updateVisibleRanges(); + }); + } + updateVisibleRanges(); + })); + this._localDisposableStore.add(this.view.onDidScroll(() => { + if (this._isInLayout) { + DOM.scheduleAtNextAnimationFrame(() => { + updateVisibleRanges(); + }); + } + updateVisibleRanges(); + })); } elementAt(position: number): ICellViewModel | undefined { @@ -374,7 +475,11 @@ export class NotebookCellList extends WorkbenchList implements ID return; } + const focusInside = DOM.isAncestor(document.activeElement, this.rowsContainer); super.splice(start, deleteCount, elements); + if (focusInside) { + this.domFocus(); + } const selectionsLeft = []; this._viewModel!.selectionHandles.forEach(handle => { @@ -398,6 +503,10 @@ export class NotebookCellList extends WorkbenchList implements ID const viewIndexInfo = this.hiddenRangesPrefixSum.getIndexOf(modelIndex); if (viewIndexInfo.remainder !== 0) { + if (modelIndex >= this.hiddenRangesPrefixSum.getTotalValue()) { + // it's already after the last hidden range + return modelIndex - (this.hiddenRangesPrefixSum.getTotalValue() - this.hiddenRangesPrefixSum.getCount()); + } return undefined; } else { return viewIndexInfo.index; @@ -413,6 +522,12 @@ export class NotebookCellList extends WorkbenchList implements ID const viewIndexInfo = this.hiddenRangesPrefixSum.getIndexOf(modelIndex); + if (viewIndexInfo.remainder !== 0) { + if (modelIndex >= this.hiddenRangesPrefixSum.getTotalValue()) { + return modelIndex - (this.hiddenRangesPrefixSum.getTotalValue() - this.hiddenRangesPrefixSum.getCount()); + } + } + return viewIndexInfo.index; } @@ -922,6 +1037,12 @@ export class NotebookCellList extends WorkbenchList implements ID } } + layout(height?: number, width?: number): void { + this._isInLayout = true; + super.layout(height, width); + this._isInLayout = false; + } + dispose() { this._isDisposed = true; this._viewModelStore.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts index 7e988c86142..41a457e3dc6 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IProcessedOutput, IRenderOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IProcessedOutput, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; import { onUnexpectedError } from 'vs/base/common/errors'; import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { URI } from 'vs/base/common/uri'; export class OutputRenderer { protected readonly _contributions: { [key: string]: IOutputTransformContribution; }; @@ -38,16 +39,14 @@ export class OutputRenderer { contentNode.innerText = `No renderer could be found for output. It has the following output type: ${output.outputKind}`; container.appendChild(contentNode); - return { - hasDynamicHeight: false - }; + return { type: RenderOutputType.None, hasDynamicHeight: false }; } - render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput { + render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput { const transform = this._mimeTypeMapping[output.outputKind]; if (transform) { - return transform.render(output, container, preferredMimeType); + return transform.render(output, container, preferredMimeType, notebookUri); } else { return this.renderNoop(output, container); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts index 7998129daa4..9e0c4ba4230 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRenderOutput, CellOutputKind, IErrorOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IRenderOutput, CellOutputKind, IErrorOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; import * as DOM from 'vs/base/browser/dom'; import { RGBA, Color } from 'vs/base/common/color'; @@ -36,9 +36,7 @@ class ErrorTransform implements IOutputTransformContribution { } container.appendChild(traceback); DOM.addClasses(container, 'error'); - return { - hasDynamicHeight: false - }; + return { type: RenderOutputType.None, hasDynamicHeight: false }; } dispose(): void { diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts index 73a1fd4af7e..b7276425242 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRenderOutput, CellOutputKind, ITransformedDisplayOutputDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IRenderOutput, CellOutputKind, ITransformedDisplayOutputDto, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; import * as DOM from 'vs/base/browser/dom'; import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -17,10 +17,10 @@ import { URI } from 'vs/base/common/uri'; import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { handleANSIOutput } from 'vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform'; +import { dirname } from 'vs/base/common/resources'; class RichRenderer implements IOutputTransformContribution { - private _mdRenderer: MarkdownRenderer; - private _richMimeTypeRenderers = new Map IRenderOutput>(); + private _richMimeTypeRenderers = new Map IRenderOutput>(); constructor( public notebookEditor: INotebookEditor, @@ -29,7 +29,6 @@ class RichRenderer implements IOutputTransformContribution { @IModeService private readonly modeService: IModeService, @IThemeService private readonly themeService: IThemeService ) { - this._mdRenderer = instantiationService.createInstance(MarkdownRenderer, undefined); this._richMimeTypeRenderers.set('application/json', this.renderJSON.bind(this)); this._richMimeTypeRenderers.set('application/javascript', this.renderJavaScript.bind(this)); this._richMimeTypeRenderers.set('text/html', this.renderHTML.bind(this)); @@ -41,15 +40,12 @@ class RichRenderer implements IOutputTransformContribution { this._richMimeTypeRenderers.set('text/x-javascript', this.renderCode.bind(this)); } - render(output: ITransformedDisplayOutputDto, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput { + render(output: ITransformedDisplayOutputDto, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI): IRenderOutput { if (!output.data) { const contentNode = document.createElement('p'); contentNode.innerText = `No data could be found for output.`; container.appendChild(contentNode); - - return { - hasDynamicHeight: false - }; + return { type: RenderOutputType.None, hasDynamicHeight: false }; } if (!preferredMimeType || !this._richMimeTypeRenderers.has(preferredMimeType)) { @@ -68,17 +64,14 @@ class RichRenderer implements IOutputTransformContribution { } container.appendChild(contentNode); - - return { - hasDynamicHeight: false - }; + return { type: RenderOutputType.None, hasDynamicHeight: false }; } const renderer = this._richMimeTypeRenderers.get(preferredMimeType); - return renderer!(output, container); + return renderer!(output, notebookUri, container); } - renderJSON(output: ITransformedDisplayOutputDto, container: HTMLElement) { + renderJSON(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { const data = output.data['application/json']; const str = JSON.stringify(data, null, '\t'); @@ -108,12 +101,10 @@ class RichRenderer implements IOutputTransformContribution { container.style.height = `${height + 16}px`; - return { - hasDynamicHeight: true - }; + return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderCode(output: ITransformedDisplayOutputDto, container: HTMLElement) { + renderCode(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { const data = output.data['text/x-javascript']; const str = (isArray(data) ? data.join('') : data) as string; @@ -143,87 +134,82 @@ class RichRenderer implements IOutputTransformContribution { container.style.height = `${height + 16}px`; - return { - hasDynamicHeight: true - }; + return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderJavaScript(output: ITransformedDisplayOutputDto, container: HTMLElement) { + renderJavaScript(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { const data = output.data['application/javascript']; const str = isArray(data) ? data.join('') : data; const scriptVal = ``; return { - shadowContent: scriptVal, + type: RenderOutputType.Html, + source: output, + htmlContent: scriptVal, hasDynamicHeight: false }; } - renderHTML(output: ITransformedDisplayOutputDto, container: HTMLElement) { + renderHTML(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { const data = output.data['text/html']; const str = (isArray(data) ? data.join('') : data) as string; return { - shadowContent: str, + type: RenderOutputType.Html, + source: output, + htmlContent: str, hasDynamicHeight: false }; - } - renderSVG(output: ITransformedDisplayOutputDto, container: HTMLElement) { + renderSVG(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { const data = output.data['image/svg+xml']; const str = (isArray(data) ? data.join('') : data) as string; return { - shadowContent: str, + type: RenderOutputType.Html, + source: output, + htmlContent: str, hasDynamicHeight: false }; } - renderMarkdown(output: ITransformedDisplayOutputDto, container: HTMLElement) { + renderMarkdown(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { const data = output.data['text/markdown']; const str = (isArray(data) ? data.join('') : data) as string; const mdOutput = document.createElement('div'); - mdOutput.appendChild(this._mdRenderer.render({ value: str, isTrusted: true, supportThemeIcons: true }).element); + const mdRenderer = this.instantiationService.createInstance(MarkdownRenderer, dirname(notebookUri)); + mdOutput.appendChild(mdRenderer.render({ value: str, isTrusted: true, supportThemeIcons: true }).element); container.appendChild(mdOutput); - return { - hasDynamicHeight: true - }; + return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderPNG(output: ITransformedDisplayOutputDto, container: HTMLElement) { + renderPNG(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { const image = document.createElement('img'); image.src = `data:image/png;base64,${output.data['image/png']}`; const display = document.createElement('div'); DOM.addClasses(display, 'display'); display.appendChild(image); container.appendChild(display); - return { - hasDynamicHeight: true - }; - + return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderJPEG(output: ITransformedDisplayOutputDto, container: HTMLElement) { + renderJPEG(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { const image = document.createElement('img'); image.src = `data:image/jpeg;base64,${output.data['image/jpeg']}`; const display = document.createElement('div'); DOM.addClasses(display, 'display'); display.appendChild(image); container.appendChild(display); - return { - hasDynamicHeight: true - }; + return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderPlainText(output: ITransformedDisplayOutputDto, container: HTMLElement) { + renderPlainText(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { const data = output.data['text/plain']; const str = (isArray(data) ? data.join('') : data) as string; const contentNode = DOM.$('.output-plaintext'); contentNode.appendChild(handleANSIOutput(str, this.themeService)); container.appendChild(contentNode); - return { - hasDynamicHeight: false - }; + return { type: RenderOutputType.None, hasDynamicHeight: false }; } dispose(): void { diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts index b3d7698c233..65f1826f750 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { IRenderOutput, CellOutputKind, IStreamOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IRenderOutput, CellOutputKind, IStreamOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -18,10 +18,7 @@ class StreamRenderer implements IOutputTransformContribution { const contentNode = DOM.$('.output-stream'); contentNode.innerText = output.text; container.appendChild(contentNode); - return { - hasDynamicHeight: false - }; - + return { type: RenderOutputType.None, hasDynamicHeight: false }; } dispose(): void { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 7f37bd88b20..3a0eab9dda2 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -16,7 +16,7 @@ import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener' import { CELL_MARGIN, CELL_RUN_GUTTER, CODE_CELL_LEFT_MARGIN, CELL_OUTPUT_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; -import { CellOutputKind, IProcessedOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellOutputKind, IDisplayOutput, IInsetRenderOutput, INotebookRendererInfo, IProcessedOutput, ITransformedDisplayOutputDto, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IWebviewService, WebviewElement, WebviewContentPurpose } from 'vs/workbench/contrib/webview/browser/webview'; import { asWebviewUri } from 'vs/workbench/contrib/webview/common/webviewUri'; @@ -88,12 +88,14 @@ export interface IClearMessage { export interface ICreationRequestMessage { type: 'html'; - content: string; + content: + | { type: RenderOutputType.Html; htmlContent: string } + | { type: RenderOutputType.Extension; output: IDisplayOutput; mimeType: string }; cellId: string; outputId: string; top: number; left: number; - requiredPreloads: IPreloadResource[]; + requiredPreloads: ReadonlyArray; initiallyHidden?: boolean; apiNamespace?: string | undefined; } @@ -200,7 +202,7 @@ export type AnyMessage = FromWebviewMessage | ToWebviewMessage; interface ICachedInset { outputId: string; cell: CodeCellViewModel; - preloads: ReadonlySet; + renderer?: INotebookRendererInfo; cachedCreation: ICreationRequestMessage; } @@ -224,11 +226,11 @@ export class BackLayerWebView extends Disposable { insetMapping: Map = new Map(); hiddenInsetMapping: Set = new Set(); reversedInsetMapping: Map = new Map(); - preloadsCache: Map = new Map(); localResourceRootsCache: URI[] | undefined = undefined; rendererRootsCache: URI[] = []; kernelRootsCache: URI[] = []; private readonly _onMessage = this._register(new Emitter()); + private readonly _preloadsCache = new Set(); public readonly onMessage: Event = this._onMessage.event; private _loaded!: Promise; private _initalized?: Promise; @@ -425,9 +427,17 @@ ${loaderJs} return; } - this.preloadsCache.clear(); + let renderers = new Set(); + for (const inset of this.insetMapping.values()) { + if (inset.renderer) { + renderers.add(inset.renderer); + } + } + + this._preloadsCache.clear(); + this.updateRendererPreloads(renderers); + for (const [output, inset] of this.insetMapping.entries()) { - this.updateRendererPreloads(inset.preloads); this._sendMessageToWebview({ ...inset.cachedCreation, initiallyHidden: this.hiddenInsetMapping.has(output) }); } })); @@ -622,19 +632,18 @@ ${loaderJs} }); } - async createInset(cell: CodeCellViewModel, output: IProcessedOutput, cellTop: number, offset: number, shadowContent: string, preloads: Set) { + async createInset(cell: CodeCellViewModel, content: IInsetRenderOutput, cellTop: number, offset: number) { if (this._disposed) { return; } - const requiredPreloads = await this.updateRendererPreloads(preloads); const initialTop = cellTop + offset; - if (this.insetMapping.has(output)) { - const outputCache = this.insetMapping.get(output); + if (this.insetMapping.has(content.source)) { + const outputCache = this.insetMapping.get(content.source); if (outputCache) { - this.hiddenInsetMapping.delete(output); + this.hiddenInsetMapping.delete(content.source); this._sendMessageToWebview({ type: 'showOutput', cellId: outputCache.cell.id, @@ -645,30 +654,49 @@ ${loaderJs} } } - const outputId = output.outputKind === CellOutputKind.Rich ? output.outputId : UUID.generateUuid(); - let apiNamespace: string | undefined; - if (output.outputKind === CellOutputKind.Rich && output.pickedMimeTypeIndex !== undefined) { - const pickedMimeTypeRenderer = output.orderedMimeTypes?.[output.pickedMimeTypeIndex]; - if (pickedMimeTypeRenderer?.rendererId) { - apiNamespace = this.notebookService.getRendererInfo(pickedMimeTypeRenderer.rendererId)?.id; - } + const messageBase = { + type: 'html', + cellId: cell.id, + top: initialTop, + left: 0, + requiredPreloads: [], + } as const; + + let message: ICreationRequestMessage; + let renderer: INotebookRendererInfo | undefined; + if (content.type === RenderOutputType.Extension) { + const output = content.source as ITransformedDisplayOutputDto; + renderer = content.renderer; + message = { + ...messageBase, + outputId: output.outputId, + apiNamespace: content.renderer.id, + requiredPreloads: await this.updateRendererPreloads([content.renderer]), + content: { + type: RenderOutputType.Extension, + mimeType: content.mimeType, + output: { + outputKind: CellOutputKind.Rich, + metadata: output.metadata, + data: output.data, + }, + }, + }; + } else { + message = { + ...messageBase, + outputId: UUID.generateUuid(), + content: { + type: content.type, + htmlContent: content.htmlContent, + } + }; } - const message: ICreationRequestMessage = { - type: 'html', - content: shadowContent, - cellId: cell.id, - apiNamespace, - outputId: outputId, - top: initialTop, - requiredPreloads, - left: 0 - }; - this._sendMessageToWebview(message); - this.insetMapping.set(output, { outputId: outputId, cell: cell, preloads, cachedCreation: message }); - this.hiddenInsetMapping.delete(output); - this.reversedInsetMapping.set(outputId, output); + this.insetMapping.set(content.source, { outputId: message.outputId, cell, renderer, cachedCreation: message }); + this.hiddenInsetMapping.delete(content.source); + this.reversedInsetMapping.set(message.outputId, content.source); } removeInset(output: IProcessedOutput) { @@ -774,9 +802,9 @@ ${loaderJs} }); preloads.forEach(e => { - if (!this.preloadsCache.has(e.toString())) { + if (!this._preloadsCache.has(e.toString())) { resources.push({ uri: e.toString() }); - this.preloadsCache.set(e.toString(), true); + this._preloadsCache.add(e.toString()); } }); @@ -788,7 +816,7 @@ ${loaderJs} this._updatePreloads(resources, 'kernel'); } - async updateRendererPreloads(preloads: ReadonlySet) { + async updateRendererPreloads(renderers: Iterable) { if (this._disposed) { return []; } @@ -798,28 +826,21 @@ ${loaderJs} const requiredPreloads: IPreloadResource[] = []; const resources: IPreloadResource[] = []; const extensionLocations: URI[] = []; - preloads.forEach(preload => { - const rendererInfo = this.notebookService.getRendererInfo(preload); + for (const rendererInfo of renderers) { + const preloads = [rendererInfo.entrypoint, ...rendererInfo.preloads] + .map(preload => asWebviewUri(this.workbenchEnvironmentService, this.id, preload)); + extensionLocations.push(rendererInfo.extensionLocation); - if (rendererInfo) { - const preloadResources = rendererInfo.preloads.map(preloadResource => { - if (this.environmentService.isExtensionDevelopment && (preloadResource.scheme === 'http' || preloadResource.scheme === 'https')) { - return preloadResource; - } - return asWebviewUri(this.workbenchEnvironmentService, this.id, preloadResource); - }); - extensionLocations.push(rendererInfo.extensionLocation); - preloadResources.forEach(e => { - const resource: IPreloadResource = { uri: e.toString() }; - requiredPreloads.push(resource); + preloads.forEach(e => { + const resource: IPreloadResource = { uri: e.toString() }; + requiredPreloads.push(resource); - if (!this.preloadsCache.has(e.toString())) { - resources.push(resource); - this.preloadsCache.set(e.toString(), true); - } - }); - } - }); + if (!this._preloadsCache.has(e.toString())) { + resources.push(resource); + this._preloadsCache.add(e.toString()); + } + }); + } if (!resources.length) { return requiredPreloads; @@ -855,7 +876,7 @@ ${loaderJs} } clearPreloadsCache() { - this.preloadsCache.clear(); + this._preloadsCache.clear(); } dispose() { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts index c5ccf32bc91..66ffa090a93 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts @@ -29,30 +29,33 @@ export class VerticalSeparatorViewItem extends BaseActionViewItem { } } -export function createAndFillInActionBarActionsWithVerticalSeparators(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, isPrimaryGroup?: (group: string) => boolean): IDisposable { +export function createAndFillInActionBarActionsWithVerticalSeparators(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, alwaysFillSecondary?: boolean, isPrimaryGroup?: (group: string) => boolean): IDisposable { const groups = menu.getActions(options); // Action bars handle alternative actions on their own so the alternative actions should be ignored - fillInActions(groups, target, false, isPrimaryGroup); + fillInActions(groups, target, false, alwaysFillSecondary, isPrimaryGroup); return asDisposable(groups); } -function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray]>, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, useAlternativeActions: boolean, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void { +function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray]>, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, useAlternativeActions: boolean, alwaysFillSecondary = false, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void { for (const tuple of groups) { let [group, actions] = tuple; if (useAlternativeActions) { actions = actions.map(a => (a instanceof MenuItemAction) && !!a.alt ? a.alt : a); } - if (isPrimaryGroup(group)) { - const to = Array.isArray(target) ? target : target.primary; + const isPrimary = isPrimaryGroup(group); + if (isPrimary) { + const to = Array.isArray(target) ? target : target.primary; if (to.length > 0) { to.push(new VerticalSeparator()); } to.push(...actions); - } else { - const to = Array.isArray(target) ? target : target.secondary; + } + + if (!isPrimary || alwaysFillSecondary) { + const to = Array.isArray(target) ? target : target.secondary; if (to.length > 0) { to.push(new Separator()); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts index 3c3b9eac7f4..945ba2e54c8 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts @@ -6,7 +6,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { INotebookTextModel, NotebookCellRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel'; -import { NOTEBOOK_CELL_TYPE, NOTEBOOK_VIEW_TYPE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_RUNNABLE, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_RUN_STATE, NOTEBOOK_CELL_HAS_OUTPUTS, CellViewModelStateChangeEvent, CellEditState, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NOTEBOOK_CELL_TYPE, NOTEBOOK_VIEW_TYPE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_RUNNABLE, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_RUN_STATE, NOTEBOOK_CELL_HAS_OUTPUTS, CellViewModelStateChangeEvent, CellEditState, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_FOCUSED, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -17,6 +17,7 @@ export class CellContextKeyManager extends Disposable { private viewType!: IContextKey; private cellEditable!: IContextKey; private cellRunnable!: IContextKey; + private cellFocused!: IContextKey; private cellRunState!: IContextKey; private cellHasOutputs!: IContextKey; private cellContentCollapsed!: IContextKey; @@ -28,6 +29,7 @@ export class CellContextKeyManager extends Disposable { constructor( private readonly contextKeyService: IContextKeyService, + private readonly notebookEditor: INotebookEditor, private readonly notebookTextModel: INotebookTextModel, private element: BaseCellViewModel ) { @@ -37,6 +39,7 @@ export class CellContextKeyManager extends Disposable { this.cellType = NOTEBOOK_CELL_TYPE.bindTo(this.contextKeyService); this.viewType = NOTEBOOK_VIEW_TYPE.bindTo(this.contextKeyService); this.cellEditable = NOTEBOOK_CELL_EDITABLE.bindTo(this.contextKeyService); + this.cellFocused = NOTEBOOK_CELL_FOCUSED.bindTo(this.contextKeyService); this.cellRunnable = NOTEBOOK_CELL_RUNNABLE.bindTo(this.contextKeyService); this.markdownEditMode = NOTEBOOK_CELL_MARKDOWN_EDIT_MODE.bindTo(this.contextKeyService); this.cellRunState = NOTEBOOK_CELL_RUN_STATE.bindTo(this.contextKeyService); @@ -57,6 +60,7 @@ export class CellContextKeyManager extends Disposable { } this.elementDisposables.add(element.model.onDidChangeMetadata(() => this.updateForCollapseState())); + this.elementDisposables.add(this.notebookEditor.onDidChangeActiveCell(() => this.updateForFocusState())); this.element = element; if (this.element instanceof MarkdownCellViewModel) { @@ -66,6 +70,7 @@ export class CellContextKeyManager extends Disposable { } this.contextKeyService.bufferChangeEvents(() => { + this.updateForFocusState(); this.updateForMetadata(); this.updateForEditState(); this.updateForCollapseState(); @@ -91,6 +96,10 @@ export class CellContextKeyManager extends Disposable { }); } + private updateForFocusState() { + this.cellFocused.set(this.notebookEditor.getActiveCell() === this.element); + } + private updateForMetadata() { const metadata = this.element.getEvaluatedMetadata(this.notebookTextModel.metadata); this.cellEditable.set(!!metadata.editable); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts index 8c3f6e513b5..acd53605379 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts @@ -19,6 +19,10 @@ export class CellMenus { return this.getMenu(MenuId.NotebookCellBetween, contextKeyService); } + getCellTopInsertionMenu(contextKeyService: IContextKeyService): IMenu { + return this.getMenu(MenuId.NotebookCellListTop, contextKeyService); + } + private getMenu(menuId: MenuId, contextKeyService: IContextKeyService): IMenu { const menu = this.menuService.createMenu(menuId, contextKeyService); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index a4bc32bcf2b..9be48cf297a 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -10,7 +10,6 @@ import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/lis import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IAction } from 'vs/base/common/actions'; -import { Delayer } from 'vs/base/common/async'; import { renderCodicons } from 'vs/base/common/codicons'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; @@ -26,7 +25,6 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ITextModel } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; import { tokenizeLineToHTML } from 'vs/editor/common/modes/textToHtmlTokenizer'; -import { IModeService } from 'vs/editor/common/services/modeService'; import { localize } from 'vs/nls'; import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenu, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; @@ -37,17 +35,20 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_TOP_MARGIN, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CancelCellAction, ChangeCellLanguageAction, ExecuteCellAction, INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; -import { BaseCellRenderTemplate, CellEditState, CodeCellRenderTemplate, EXPAND_CELL_CONTENT_COMMAND_ID, ICellViewModel, INotebookCellList, INotebookEditor, isCodeCellRenderTemplate, MarkdownCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { BOTTOM_CELL_TOOLBAR_GAP, CELL_BOTTOM_MARGIN, CELL_TOP_MARGIN, EDITOR_BOTTOM_PADDING, EDITOR_BOTTOM_PADDING_WITHOUT_STATUSBAR, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CancelCellAction, DeleteCellAction, ExecuteCellAction, INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { BaseCellRenderTemplate, CellEditState, CodeCellRenderTemplate, EXPAND_CELL_CONTENT_COMMAND_ID, ICellViewModel, INotebookEditor, isCodeCellRenderTemplate, MarkdownCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys'; import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; +import { CellEditorStatusBar } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets'; import { CodeCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/codeCell'; +import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents'; +import { CellDragAndDropController, DRAGGING_CLASS } from 'vs/workbench/contrib/notebook/browser/view/renderers/dnd'; import { StatefulMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/markdownCell'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind, NotebookCellMetadata, NotebookCellRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, NotebookCellRunState, ShowCellStatusbarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { createAndFillInActionBarActionsWithVerticalSeparators, VerticalSeparator, VerticalSeparatorViewItem } from './cellActionView'; const $ = DOM.$; @@ -79,29 +80,9 @@ export class NotebookCellListDelegate implements IListVirtualDelegate { - if (e.affectsConfiguration('editor')) { + if (e.affectsConfiguration('editor') || e.affectsConfiguration(ShowCellStatusbarKey)) { this._value = computeEditorOptions(); this._onDidChange.fire(this.value); } }); const computeEditorOptions = () => { + const showCellStatusBar = configurationService.getValue(ShowCellStatusbarKey); + const editorPadding = { + top: EDITOR_TOP_PADDING, + bottom: showCellStatusBar ? EDITOR_BOTTOM_PADDING : EDITOR_BOTTOM_PADDING_WITHOUT_STATUSBAR + }; + const editorOptions = deepClone(configurationService.getValue('editor', { overrideIdentifier: language })); const computed = { ...editorOptions, - ...CellEditorOptions.fixedEditorOptions + ...CellEditorOptions.fixedEditorOptions, + ...{ padding: editorPadding } }; if (!computed.folding) { @@ -209,7 +197,7 @@ abstract class AbstractCellRenderer { const cellMenu = this.instantiationService.createInstance(CellMenus); const menu = disposables.add(cellMenu.getCellInsertionMenu(contextKeyService)); - const actions = this.getCellToolbarActions(menu); + const actions = this.getCellToolbarActions(menu, false); toolbar.setActions(actions.primary, actions.secondary); return toolbar; @@ -243,7 +231,8 @@ abstract class AbstractCellRenderer { } return undefined; - } + }, + renderDropdownAsChildElement: true }); if (elementClass) { @@ -253,19 +242,19 @@ abstract class AbstractCellRenderer { return toolbar; } - private getCellToolbarActions(menu: IMenu): { primary: IAction[], secondary: IAction[] } { + private getCellToolbarActions(menu: IMenu, alwaysFillSecondaryActions: boolean): { primary: IAction[], secondary: IAction[] } { const primary: IAction[] = []; const secondary: IAction[] = []; const result = { primary, secondary }; - createAndFillInActionBarActionsWithVerticalSeparators(menu, { shouldForwardArgs: true }, result, g => /^inline/.test(g)); + createAndFillInActionBarActionsWithVerticalSeparators(menu, { shouldForwardArgs: true }, result, alwaysFillSecondaryActions, g => /^inline/.test(g)); return result; } protected setupCellToolbarActions(templateData: BaseCellRenderTemplate, disposables: DisposableStore): void { const updateActions = () => { - const actions = this.getCellToolbarActions(templateData.titleMenu); + const actions = this.getCellToolbarActions(templateData.titleMenu, true); const hadFocus = DOM.isAncestor(document.activeElement, templateData.toolbar.getElement()); templateData.toolbar.setActions(actions.primary, actions.secondary); @@ -329,7 +318,7 @@ abstract class AbstractCellRenderer { this.addExpandListener(templateData); } - protected commonRenderElement(element: ICellViewModel, index: number, templateData: BaseCellRenderTemplate): void { + protected commonRenderElement(element: ICellViewModel, templateData: BaseCellRenderTemplate): void { if (element.dragging) { templateData.container.classList.add(DRAGGING_CLASS); } else { @@ -344,9 +333,9 @@ abstract class AbstractCellRenderer { } if (templateData.currentRenderedCell.metadata?.inputCollapsed) { - this.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(templateData.currentRenderedCell.handle, { inputCollapsed: false }); + this.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(templateData.currentRenderedCell.handle, { inputCollapsed: false }); } else if (templateData.currentRenderedCell.metadata?.outputCollapsed) { - this.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(templateData.currentRenderedCell.handle, { outputCollapsed: false }); + this.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(templateData.currentRenderedCell.handle, { outputCollapsed: false }); } })); } @@ -394,7 +383,12 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR const container = DOM.append(rootContainer, DOM.$('.cell-inner-container')); const disposables = new DisposableStore(); const contextKeyService = disposables.add(this.contextKeyServiceProvider(container)); - const toolbar = disposables.add(this.createToolbar(container, 'cell-title-toolbar')); + const decorationContainer = DOM.append(container, $('.cell-decoration')); + const titleToolbarContainer = DOM.append(container, $('.cell-title-toolbar')); + const toolbar = disposables.add(this.createToolbar(titleToolbarContainer)); + const deleteToolbar = disposables.add(this.createToolbar(titleToolbarContainer, 'cell-delete-toolbar')); + deleteToolbar.setActions([this.instantiationService.createInstance(DeleteCellAction)]); + const focusIndicatorLeft = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-left')); const codeInnerContent = DOM.append(container, $('.cell.code')); @@ -410,7 +404,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR const bottomCellContainer = DOM.append(container, $('.cell-bottom-toolbar-container')); const betweenCellToolbar = disposables.add(this.createBetweenCellToolbar(bottomCellContainer, disposables, contextKeyService)); - const statusBar = this.instantiationService.createInstance(CellEditorStatusBar, editorPart); + const statusBar = disposables.add(this.instantiationService.createInstance(CellEditorStatusBar, editorPart)); const titleMenu = disposables.add(this.cellMenus.getCellTitleMenu(contextKeyService)); const templateData: MarkdownCellRenderTemplate = { @@ -418,6 +412,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR expandButton, contextKeyService, container, + decorationContainer, cellContainer: innerContent, editorPart, editorContainer, @@ -426,11 +421,11 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR disposables, elementDisposables: new DisposableStore(), toolbar, + deleteToolbar, betweenCellToolbar, bottomCellContainer, - statusBarContainer: statusBar.statusBarContainer, - languageStatusBarItem: statusBar.languageStatusBarItem, titleMenu, + statusBar, toJSON: () => { return {}; } }; this.dndController.registerDragHandle(templateData, rootContainer, container, () => this.getDragImage(templateData)); @@ -466,16 +461,12 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR } renderElement(element: MarkdownCellViewModel, index: number, templateData: MarkdownCellRenderTemplate, height: number | undefined): void { - this.commonRenderElement(element, index, templateData); + this.commonRenderElement(element, templateData); templateData.currentRenderedCell = element; templateData.currentEditor = undefined; templateData.editorPart!.style.display = 'none'; - templateData.cellContainer.innerHTML = ''; - const renderedHTML = element.getHTML(); - if (renderedHTML) { - templateData.cellContainer.appendChild(renderedHTML); - } + templateData.cellContainer.innerText = ''; if (height === undefined) { return; @@ -483,7 +474,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR const elementDisposables = templateData.elementDisposables; - elementDisposables.add(new CellContextKeyManager(templateData.contextKeyService, this.notebookEditor.viewModel?.notebookDocument!, element)); + elementDisposables.add(new CellContextKeyManager(templateData.contextKeyService, this.notebookEditor, this.notebookEditor.viewModel?.notebookDocument!, element)); // render toolbar first this.setupCellToolbarActions(templateData, elementDisposables); @@ -494,6 +485,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR $mid: 12 }; templateData.toolbar.context = toolbarContext; + templateData.deleteToolbar.context = toolbarContext; this.setBetweenCellToolbarContext(templateData, element, toolbarContext); @@ -502,7 +494,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR elementDisposables.add(this.editorOptions.onDidChange(newValue => markdownCell.updateEditorOptions(newValue))); elementDisposables.add(markdownCell); - templateData.languageStatusBarItem.update(element, this.notebookEditor); + templateData.statusBar.update(toolbarContext); } disposeTemplate(templateData: MarkdownCellRenderTemplate): void { @@ -519,317 +511,6 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR } } -const DRAGGING_CLASS = 'cell-dragging'; -const GLOBAL_DRAG_CLASS = 'global-drag-active'; - -type DragImageProvider = () => HTMLElement; - -interface CellDragEvent { - browserEvent: DragEvent; - draggedOverCell: ICellViewModel; - cellTop: number; - cellHeight: number; - dragPosRatio: number; -} - -export class CellDragAndDropController extends Disposable { - // TODO@roblourens - should probably use dataTransfer here, but any dataTransfer set makes the editor think I am dropping a file, need - // to figure out how to prevent that - private currentDraggedCell: ICellViewModel | undefined; - - private listInsertionIndicator: HTMLElement; - - private list!: INotebookCellList; - - private isScrolling = false; - private scrollingDelayer: Delayer; - - constructor( - private readonly notebookEditor: INotebookEditor, - insertionIndicatorContainer: HTMLElement - ) { - super(); - - this.listInsertionIndicator = DOM.append(insertionIndicatorContainer, $('.cell-list-insertion-indicator')); - - this._register(domEvent(document.body, DOM.EventType.DRAG_START, true)(this.onGlobalDragStart.bind(this))); - this._register(domEvent(document.body, DOM.EventType.DRAG_END, true)(this.onGlobalDragEnd.bind(this))); - - const addCellDragListener = (eventType: string, handler: (e: CellDragEvent) => void) => { - this._register(DOM.addDisposableListener( - notebookEditor.getDomNode(), - eventType, - e => { - const cellDragEvent = this.toCellDragEvent(e); - if (cellDragEvent) { - handler(cellDragEvent); - } - })); - }; - - addCellDragListener(DOM.EventType.DRAG_OVER, event => { - event.browserEvent.preventDefault(); - this.onCellDragover(event); - }); - addCellDragListener(DOM.EventType.DROP, event => { - event.browserEvent.preventDefault(); - this.onCellDrop(event); - }); - addCellDragListener(DOM.EventType.DRAG_LEAVE, event => { - event.browserEvent.preventDefault(); - this.onCellDragLeave(event); - }); - - this.scrollingDelayer = new Delayer(200); - } - - setList(value: INotebookCellList) { - this.list = value; - - this.list.onWillScroll(e => { - if (!e.scrollTopChanged) { - return; - } - - this.setInsertIndicatorVisibility(false); - this.isScrolling = true; - this.scrollingDelayer.trigger(() => { - this.isScrolling = false; - }); - }); - } - - private setInsertIndicatorVisibility(visible: boolean) { - this.listInsertionIndicator.style.opacity = visible ? '1' : '0'; - } - - private toCellDragEvent(event: DragEvent): CellDragEvent | undefined { - const targetTop = this.notebookEditor.getDomNode().getBoundingClientRect().top; - const dragOffset = this.list.scrollTop + event.clientY - targetTop; - const draggedOverCell = this.list.elementAt(dragOffset); - if (!draggedOverCell) { - return undefined; - } - - const cellTop = this.list.getAbsoluteTopOfElement(draggedOverCell); - const cellHeight = this.list.elementHeight(draggedOverCell); - - const dragPosInElement = dragOffset - cellTop; - const dragPosRatio = dragPosInElement / cellHeight; - - return { - browserEvent: event, - draggedOverCell, - cellTop, - cellHeight, - dragPosRatio - }; - } - - clearGlobalDragState() { - this.notebookEditor.getDomNode().classList.remove(GLOBAL_DRAG_CLASS); - } - - private onGlobalDragStart() { - this.notebookEditor.getDomNode().classList.add(GLOBAL_DRAG_CLASS); - } - - private onGlobalDragEnd() { - this.notebookEditor.getDomNode().classList.remove(GLOBAL_DRAG_CLASS); - } - - private onCellDragover(event: CellDragEvent): void { - if (!event.browserEvent.dataTransfer) { - return; - } - - if (!this.currentDraggedCell) { - event.browserEvent.dataTransfer.dropEffect = 'none'; - return; - } - - if (this.isScrolling || this.currentDraggedCell === event.draggedOverCell) { - this.setInsertIndicatorVisibility(false); - return; - } - - const dropDirection = this.getDropInsertDirection(event); - const insertionIndicatorAbsolutePos = dropDirection === 'above' ? event.cellTop : event.cellTop + event.cellHeight; - const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop + BOTTOM_CELL_TOOLBAR_HEIGHT / 2; - if (insertionIndicatorTop >= 0) { - this.listInsertionIndicator.style.top = `${insertionIndicatorTop}px`; - this.setInsertIndicatorVisibility(true); - } else { - this.setInsertIndicatorVisibility(false); - } - } - - private getDropInsertDirection(event: CellDragEvent): 'above' | 'below' { - return event.dragPosRatio < 0.5 ? 'above' : 'below'; - } - - private onCellDrop(event: CellDragEvent): void { - const draggedCell = this.currentDraggedCell!; - - if (this.isScrolling || this.currentDraggedCell === event.draggedOverCell) { - return; - } - - let draggedCells: ICellViewModel[] = [draggedCell]; - - if (draggedCell.cellKind === CellKind.Markdown) { - const currCellIndex = this.notebookEditor.viewModel!.getCellIndex(draggedCell); - const nextVisibleCellIndex = this.notebookEditor.viewModel!.getNextVisibleCellIndex(currCellIndex); - - if (nextVisibleCellIndex > currCellIndex + 1) { - // folding ;) - draggedCells = this.notebookEditor.viewModel!.viewCells.slice(currCellIndex, nextVisibleCellIndex); - } - } - - this.dragCleanup(); - - const isCopy = (event.browserEvent.ctrlKey && !platform.isMacintosh) || (event.browserEvent.altKey && platform.isMacintosh); - - const dropDirection = this.getDropInsertDirection(event); - const insertionIndicatorAbsolutePos = dropDirection === 'above' ? event.cellTop : event.cellTop + event.cellHeight; - const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop + BOTTOM_CELL_TOOLBAR_HEIGHT / 2; - const editorHeight = this.notebookEditor.getDomNode().getBoundingClientRect().height; - if (insertionIndicatorTop < 0 || insertionIndicatorTop > editorHeight) { - // Ignore drop, insertion point is off-screen - return; - } - - if (isCopy) { - this.copyCells(draggedCells, event.draggedOverCell, dropDirection); - } else { - this.moveCells(draggedCells, event.draggedOverCell, dropDirection); - } - } - - private onCellDragLeave(event: CellDragEvent): void { - if (!event.browserEvent.relatedTarget || !DOM.isAncestor(event.browserEvent.relatedTarget as HTMLElement, this.notebookEditor.getDomNode())) { - this.setInsertIndicatorVisibility(false); - } - } - - private dragCleanup(): void { - if (this.currentDraggedCell) { - this.currentDraggedCell.dragging = false; - this.currentDraggedCell = undefined; - } - - this.setInsertIndicatorVisibility(false); - } - - registerDragHandle(templateData: BaseCellRenderTemplate, cellRoot: HTMLElement, dragHandle: HTMLElement, dragImageProvider: DragImageProvider): void { - const container = templateData.container; - dragHandle.setAttribute('draggable', 'true'); - - templateData.disposables.add(domEvent(dragHandle, DOM.EventType.DRAG_END)(() => { - // Note, templateData may have a different element rendered into it by now - container.classList.remove(DRAGGING_CLASS); - this.dragCleanup(); - })); - - templateData.disposables.add(domEvent(dragHandle, DOM.EventType.DRAG_START)(event => { - if (!event.dataTransfer) { - return; - } - - this.currentDraggedCell = templateData.currentRenderedCell!; - this.currentDraggedCell.dragging = true; - - const dragImage = dragImageProvider(); - cellRoot.parentElement!.appendChild(dragImage); - event.dataTransfer.setDragImage(dragImage, 0, 0); - setTimeout(() => cellRoot.parentElement!.removeChild(dragImage!), 0); // Comment this out to debug drag image layout - - container.classList.add(DRAGGING_CLASS); - })); - } - - private async moveCells(draggedCells: ICellViewModel[], ontoCell: ICellViewModel, direction: 'above' | 'below') { - this.notebookEditor.textModel!.pushStackElement('Move Cells'); - if (direction === 'above') { - for (let i = 0; i < draggedCells.length; i++) { - const relativeToIndex = this.notebookEditor!.viewModel!.getCellIndex(ontoCell); - const newIdx = relativeToIndex; - - await this.notebookEditor.moveCellToIdx(draggedCells[i], newIdx); - } - } else { - for (let i = draggedCells.length - 1; i >= 0; i--) { - const relativeToIndex = this.notebookEditor!.viewModel!.getCellIndex(ontoCell); - const newIdx = relativeToIndex + 1; - await this.notebookEditor.moveCellToIdx(draggedCells[i], newIdx); - } - } - this.notebookEditor.textModel!.pushStackElement('Move Cells'); - } - - private copyCells(draggedCells: ICellViewModel[], ontoCell: ICellViewModel, direction: 'above' | 'below') { - this.notebookEditor.textModel!.pushStackElement('Copy Cells'); - let firstNewCell: ICellViewModel | undefined = undefined; - let firstNewCellState: CellEditState = CellEditState.Preview; - for (let i = 0; i < draggedCells.length; i++) { - const draggedCell = draggedCells[i]; - const newCell = this.notebookEditor.insertNotebookCell(ontoCell, draggedCell.cellKind, direction, draggedCell.getText()); - - if (newCell && !firstNewCell) { - firstNewCell = newCell; - firstNewCellState = draggedCell.editState; - } - } - - if (firstNewCell) { - this.notebookEditor.focusNotebookCell(firstNewCell, firstNewCellState === CellEditState.Editing ? 'editor' : 'container'); - } - - this.notebookEditor.textModel!.pushStackElement('Copy Cells'); - } -} - -export class CellLanguageStatusBarItem extends Disposable { - private readonly labelElement: HTMLElement; - - private cell: ICellViewModel | undefined; - private editor: INotebookEditor | undefined; - - private cellDisposables: DisposableStore; - - constructor( - readonly container: HTMLElement, - @IModeService private readonly modeService: IModeService, - @IInstantiationService private readonly instantiationService: IInstantiationService - ) { - super(); - this.labelElement = DOM.append(container, $('.cell-language-picker')); - this.labelElement.tabIndex = 0; - - this._register(DOM.addDisposableListener(this.labelElement, DOM.EventType.CLICK, () => { - this.instantiationService.invokeFunction(accessor => { - new ChangeCellLanguageAction().run(accessor, { notebookEditor: this.editor!, cell: this.cell! }); - }); - })); - this._register(this.cellDisposables = new DisposableStore()); - } - - update(cell: ICellViewModel, editor: INotebookEditor): void { - this.cellDisposables.clear(); - this.cell = cell; - this.editor = editor; - - this.render(); - this.cellDisposables.add(this.cell.model.onDidChangeLanguage(() => this.render())); - } - - private render(): void { - const modeId = this.modeService.getModeIdForLanguageName(this.cell!.language) || this.cell!.language; - this.labelElement.textContent = this.modeService.getLanguageName(modeId) || this.modeService.getLanguageName('plaintext'); - } -} - class EditorTextRenderer { getRichText(editor: ICodeEditor, modelRange: Range): string | null { @@ -925,27 +606,6 @@ class CodeCellDragImageRenderer { } } -class CellEditorStatusBar { - readonly cellStatusMessageContainer: HTMLElement; - readonly cellRunStatusContainer: HTMLElement; - readonly statusBarContainer: HTMLElement; - readonly languageStatusBarItem: CellLanguageStatusBarItem; - readonly durationContainer: HTMLElement; - - constructor( - container: HTMLElement, - @IInstantiationService instantiationService: IInstantiationService - ) { - this.statusBarContainer = DOM.append(container, $('.cell-statusbar-container')); - const leftStatusBarItems = DOM.append(this.statusBarContainer, $('.cell-status-left')); - const rightStatusBarItems = DOM.append(this.statusBarContainer, $('.cell-status-right')); - this.cellRunStatusContainer = DOM.append(leftStatusBarItems, $('.cell-run-status')); - this.durationContainer = DOM.append(leftStatusBarItems, $('.cell-run-duration')); - this.cellStatusMessageContainer = DOM.append(leftStatusBarItems, $('.cell-status-message')); - this.languageStatusBarItem = instantiationService.createInstance(CellLanguageStatusBarItem, rightStatusBarItems); - } -} - export class CodeCellRenderer extends AbstractCellRenderer implements IListRenderer { static readonly TEMPLATE_ID = 'code_cell'; @@ -972,17 +632,20 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const container = DOM.append(rootContainer, DOM.$('.cell-inner-container')); const disposables = new DisposableStore(); const contextKeyService = disposables.add(this.contextKeyServiceProvider(container)); - + const decorationContainer = DOM.append(container, $('.cell-decoration')); DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-top')); - const toolbar = disposables.add(this.createToolbar(container, 'cell-title-toolbar')); + const titleToolbarContainer = DOM.append(container, $('.cell-title-toolbar')); + const toolbar = disposables.add(this.createToolbar(titleToolbarContainer)); + const deleteToolbar = disposables.add(this.createToolbar(titleToolbarContainer, 'cell-delete-toolbar')); + deleteToolbar.setActions([this.instantiationService.createInstance(DeleteCellAction)]); + const focusIndicator = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-left')); const dragHandle = DOM.append(container, DOM.$('.cell-drag-handle')); const cellContainer = DOM.append(container, $('.cell.code')); - const runButtonContainer = DOM.append(cellContainer, $('.run-button-container')); - const runToolbar = this.createToolbar(runButtonContainer); - disposables.add(runToolbar); + const runButtonContainer = DOM.append(container, $('.run-button-container')); + const runToolbar = disposables.add(this.createToolbar(runButtonContainer)); const executionOrderLabel = DOM.append(runButtonContainer, $('div.execution-count-label')); // create a special context key service that set the inCompositeEditor-contextkey @@ -998,7 +661,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende width: 0, height: 0 }, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() + // overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() }, {}); disposables.add(this.editorOptions.onDidChange(newValue => editor.updateOptions(newValue))); @@ -1009,7 +672,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende progressBar.hide(); disposables.add(progressBar); - const statusBar = this.instantiationService.createInstance(CellEditorStatusBar, editorPart); + const statusBar = disposables.add(this.instantiationService.createInstance(CellEditorStatusBar, editorPart)); const timer = new TimerRenderer(statusBar.durationContainer); const cellRunState = new RunStateRenderer(statusBar.cellRunStatusContainer, runToolbar, this.instantiationService); @@ -1031,16 +694,16 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende expandButton, contextKeyService, container, + decorationContainer, cellContainer, - statusBarContainer: statusBar.statusBarContainer, cellRunState, - cellStatusMessageContainer: statusBar.cellStatusMessageContainer, - languageStatusBarItem: statusBar.languageStatusBarItem, progressBar, + statusBar, focusIndicatorLeft: focusIndicator, focusIndicatorRight, focusIndicatorBottom, toolbar, + deleteToolbar, betweenCellToolbar, focusSinkElement, runToolbar, @@ -1080,9 +743,9 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende private updateForMetadata(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void { const metadata = element.getEvaluatedMetadata(this.notebookEditor.viewModel!.notebookDocument.metadata); - DOM.toggleClass(templateData.cellContainer, 'runnable', !!metadata.runnable); + DOM.toggleClass(templateData.container, 'runnable', !!metadata.runnable); this.updateExecutionOrder(metadata, templateData); - templateData.cellStatusMessageContainer.textContent = metadata?.statusMessage || ''; + templateData.statusBar.cellStatusMessageContainer.textContent = metadata?.statusMessage || ''; templateData.cellRunState.renderState(element.metadata?.runState); @@ -1127,9 +790,9 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende private updateForLayout(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void { templateData.focusIndicatorLeft.style.height = `${element.layoutInfo.indicatorHeight}px`; templateData.focusIndicatorRight.style.height = `${element.layoutInfo.indicatorHeight}px`; - templateData.focusIndicatorBottom.style.top = `${element.layoutInfo.totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT - CELL_BOTTOM_MARGIN}px`; + templateData.focusIndicatorBottom.style.top = `${element.layoutInfo.totalHeight - BOTTOM_CELL_TOOLBAR_GAP - CELL_BOTTOM_MARGIN}px`; templateData.outputContainer.style.top = `${element.layoutInfo.outputContainerOffset}px`; - templateData.dragHandle.style.height = `${element.layoutInfo.totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT}px`; + templateData.dragHandle.style.height = `${element.layoutInfo.totalHeight - BOTTOM_CELL_TOOLBAR_GAP}px`; } renderElement(element: CodeCellViewModel, index: number, templateData: CodeCellRenderTemplate, height: number | undefined): void { @@ -1144,7 +807,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende templateData.container.classList.remove(className); }); - this.commonRenderElement(element, index, templateData); + this.commonRenderElement(element, templateData); templateData.currentRenderedCell = element; @@ -1152,14 +815,14 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende return; } - templateData.outputContainer.innerHTML = ''; + templateData.outputContainer.innerText = ''; const elementDisposables = templateData.elementDisposables; elementDisposables.add(this.instantiationService.createInstance(CodeCell, this.notebookEditor, element, templateData)); this.renderedEditors.set(element, templateData.editor); - elementDisposables.add(new CellContextKeyManager(templateData.contextKeyService, this.notebookEditor.viewModel?.notebookDocument!, element)); + elementDisposables.add(new CellContextKeyManager(templateData.contextKeyService, this.notebookEditor, this.notebookEditor.viewModel?.notebookDocument!, element)); this.updateForLayout(element, templateData); elementDisposables.add(element.onDidChangeLayout(() => { @@ -1192,10 +855,11 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende }; templateData.toolbar.context = toolbarContext; templateData.runToolbar.context = toolbarContext; + templateData.deleteToolbar.context = toolbarContext; this.setBetweenCellToolbarContext(templateData, element, toolbarContext); - templateData.languageStatusBarItem.update(element, this.notebookEditor); + templateData.statusBar.update(toolbarContext); } disposeTemplate(templateData: CodeCellRenderTemplate): void { @@ -1299,7 +963,75 @@ export class RunStateRenderer { } }, RunStateRenderer.MIN_SPINNER_TIME); } else { - this.element.innerHTML = ''; + this.element.innerText = ''; } } } + +export class ListTopCellToolbar extends Disposable { + private topCellToolbar: HTMLElement; + private _modelDisposables = new DisposableStore(); + constructor( + protected readonly notebookEditor: INotebookEditor, + + insertionIndicatorContainer: HTMLElement, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IContextMenuService protected readonly contextMenuService: IContextMenuService, + @IKeybindingService private readonly keybindingService: IKeybindingService, + @INotificationService private readonly notificationService: INotificationService, + @IContextKeyService readonly contextKeyService: IContextKeyService, + ) { + super(); + + this.topCellToolbar = DOM.append(insertionIndicatorContainer, $('.cell-list-top-cell-toolbar-container')); + + const toolbar = new ToolBar(this.topCellToolbar, this.contextMenuService, { + actionViewItemProvider: action => { + if (action instanceof MenuItemAction) { + const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + return item; + } + + return undefined; + } + }); + + const cellMenu = this.instantiationService.createInstance(CellMenus); + const menu = this._register(cellMenu.getCellTopInsertionMenu(contextKeyService)); + + const actions = this.getCellToolbarActions(menu, false); + toolbar.setActions(actions.primary, actions.secondary); + + this._register(toolbar); + + this._register(this.notebookEditor.onDidChangeModel(() => { + this._modelDisposables.clear(); + + if (this.notebookEditor.viewModel) { + this._modelDisposables.add(this.notebookEditor.viewModel.onDidChangeViewCells(() => { + this.updateClass(); + })); + } + })); + + this.updateClass(); + } + + private updateClass() { + if (this.notebookEditor.viewModel?.length === 0) { + DOM.addClass(this.topCellToolbar, 'emptyNotebook'); + } else { + DOM.removeClass(this.topCellToolbar, 'emptyNotebook'); + } + } + + private getCellToolbarActions(menu: IMenu, alwaysFillSecondaryActions: boolean): { primary: IAction[], secondary: IAction[] } { + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + + createAndFillInActionBarActionsWithVerticalSeparators(menu, { shouldForwardArgs: true }, result, alwaysFillSecondaryActions, g => /^inline/.test(g)); + + return result; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts new file mode 100644 index 00000000000..9f86d687c07 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts @@ -0,0 +1,217 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; +import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; +import { stripCodicons } from 'vs/base/common/codicons'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { extUri } from 'vs/base/common/resources'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { localize } from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ChangeCellLanguageAction, INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; +import { CellKind, CellStatusbarAlignment, INotebookCellStatusBarEntry } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +const $ = DOM.$; + +export class CellEditorStatusBar extends Disposable { + readonly cellStatusMessageContainer: HTMLElement; + readonly cellRunStatusContainer: HTMLElement; + readonly statusBarContainer: HTMLElement; + readonly languageStatusBarItem: CellLanguageStatusBarItem; + readonly durationContainer: HTMLElement; + + private readonly leftContributedItemsContainer: HTMLElement; + private readonly rightContributedItemsContainer: HTMLElement; + private readonly itemsDisposable: DisposableStore; + + private currentContext: INotebookCellActionContext | undefined; + + constructor( + container: HTMLElement, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @INotebookCellStatusBarService private readonly notebookCellStatusBarService: INotebookCellStatusBarService + ) { + super(); + this.statusBarContainer = DOM.append(container, $('.cell-statusbar-container')); + const leftItemsContainer = DOM.append(this.statusBarContainer, $('.cell-status-left')); + const rightItemsContainer = DOM.append(this.statusBarContainer, $('.cell-status-right')); + this.cellRunStatusContainer = DOM.append(leftItemsContainer, $('.cell-run-status')); + this.durationContainer = DOM.append(leftItemsContainer, $('.cell-run-duration')); + this.cellStatusMessageContainer = DOM.append(leftItemsContainer, $('.cell-status-message')); + this.leftContributedItemsContainer = DOM.append(leftItemsContainer, $('.cell-contributed-items-left')); + this.rightContributedItemsContainer = DOM.append(rightItemsContainer, $('.cell-contributed-items-right')); + this.languageStatusBarItem = instantiationService.createInstance(CellLanguageStatusBarItem, rightItemsContainer); + + this.itemsDisposable = this._register(new DisposableStore()); + this._register(this.notebookCellStatusBarService.onDidChangeEntriesForCell(e => { + if (this.currentContext && extUri.isEqual(e, this.currentContext.cell.uri)) { + this.updateStatusBarItems(); + } + })); + } + + update(context: INotebookCellActionContext) { + this.currentContext = context; + this.languageStatusBarItem.update(context.cell, context.notebookEditor); + this.updateStatusBarItems(); + } + + layout(width: number): void { + this.statusBarContainer.style.width = `${width}px`; + } + + private updateStatusBarItems() { + if (!this.currentContext) { + return; + } + + this.leftContributedItemsContainer.innerHTML = ''; + this.rightContributedItemsContainer.innerHTML = ''; + this.itemsDisposable.clear(); + + const items = this.notebookCellStatusBarService.getEntries(this.currentContext.cell.uri); + items.sort((itemA, itemB) => { + return (itemB.priority ?? 0) - (itemA.priority ?? 0); + }); + items.forEach(item => { + const itemView = this.itemsDisposable.add(this.instantiationService.createInstance(CellStatusBarItem, this.currentContext!, item)); + if (item.alignment === CellStatusbarAlignment.LEFT) { + this.leftContributedItemsContainer.appendChild(itemView.container); + } else { + this.rightContributedItemsContainer.appendChild(itemView.container); + } + }); + } +} + +class CellStatusBarItem extends Disposable { + + readonly container = $('.cell-status-item'); + + constructor( + private readonly _context: INotebookCellActionContext, + private readonly _itemModel: INotebookCellStatusBarEntry, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @ICommandService private readonly commandService: ICommandService, + @INotificationService private readonly notificationService: INotificationService + ) { + super(); + new CodiconLabel(this.container).text = this._itemModel.text; + + let ariaLabel: string; + let role: string | undefined; + if (this._itemModel.accessibilityInformation) { + ariaLabel = this._itemModel.accessibilityInformation.label; + role = this._itemModel.accessibilityInformation.role; + } else { + ariaLabel = this._itemModel.text ? stripCodicons(this._itemModel.text).trim() : ''; + } + + if (ariaLabel) { + this.container.setAttribute('aria-label', ariaLabel); + } + + if (role) { + this.container.setAttribute('role', role); + } + + this.container.title = this._itemModel.tooltip ?? ''; + + if (this._itemModel.command) { + this.container.classList.add('cell-status-item-has-command'); + this.container.tabIndex = 0; + + this._register(DOM.addDisposableListener(this.container, DOM.EventType.CLICK, _e => { + this.executeCommand(); + })); + this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_UP, e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) { + this.executeCommand(); + } + })); + } + } + + private async executeCommand(): Promise { + const command = this._itemModel.command; + if (!command) { + return; + } + + const id = typeof command === 'string' ? command : command.id; + const args = typeof command === 'string' ? [] : command.arguments ?? []; + + args.unshift(this._context); + + this.telemetryService.publicLog2('workbenchActionExecuted', { id, from: 'cell status bar' }); + try { + await this.commandService.executeCommand(id, ...args); + } catch (error) { + this.notificationService.error(toErrorMessage(error)); + } + } +} + +export class CellLanguageStatusBarItem extends Disposable { + private readonly labelElement: HTMLElement; + + private cell: ICellViewModel | undefined; + private editor: INotebookEditor | undefined; + + private cellDisposables: DisposableStore; + + constructor( + readonly container: HTMLElement, + @IModeService private readonly modeService: IModeService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + super(); + this.labelElement = DOM.append(container, $('.cell-language-picker.cell-status-item')); + this.labelElement.tabIndex = 0; + + this._register(DOM.addDisposableListener(this.labelElement, DOM.EventType.CLICK, () => { + this.run(); + })); + this._register(DOM.addDisposableListener(this.labelElement, DOM.EventType.KEY_UP, e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) { + this.run(); + } + })); + this._register(this.cellDisposables = new DisposableStore()); + } + + private run() { + this.instantiationService.invokeFunction(accessor => { + new ChangeCellLanguageAction().run(accessor, { notebookEditor: this.editor!, cell: this.cell! }); + }); + } + + update(cell: ICellViewModel, editor: INotebookEditor): void { + this.cellDisposables.clear(); + this.cell = cell; + this.editor = editor; + + this.render(); + this.cellDisposables.add(this.cell.model.onDidChangeLanguage(() => this.render())); + } + + private render(): void { + const modeId = this.cell?.cellKind === CellKind.Markdown ? 'markdown' : this.modeService.getModeIdForLanguageName(this.cell!.language) || this.cell!.language; + this.labelElement.textContent = this.modeService.getLanguageName(modeId) || this.modeService.getLanguageName('plaintext'); + this.labelElement.title = localize('notebook.cell.status.language', "Select Cell Language Mode"); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts index c541cb9b136..64977d72610 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts @@ -4,21 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { raceCancellation } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IDimension } from 'vs/editor/common/editorCommon'; import { IModeService } from 'vs/editor/common/services/modeService'; import * as nls from 'vs/nls'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { EDITOR_BOTTOM_PADDING, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; import { CellFocusMode, CodeCellRenderTemplate, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; -import { CellOutputKind, IProcessedOutput, IRenderOutput, ITransformedDisplayOutputDto, BUILTIN_RENDERER_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { IDimension } from 'vs/editor/common/editorCommon'; +import { BUILTIN_RENDERER_ID, CellOutputKind, CellUri, IInsetRenderOutput, IProcessedOutput, IRenderOutput, ITransformedDisplayOutputDto, outputHasDynamicHeight, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; interface IMimeTypeRenderer extends IQuickPickItem { index: number; @@ -32,6 +33,7 @@ interface IRenderedOutput { export class CodeCell extends Disposable { private outputResizeListeners = new Map(); private outputElements = new Map(); + constructor( private notebookEditor: INotebookEditor, private viewCell: CodeCellViewModel, @@ -193,7 +195,7 @@ export class CodeCell extends Disposable { // newly added element const currIndex = this.viewCell.outputs.indexOf(output); this.renderOutput(output, currIndex, prevElement); - prevElement = this.outputElements.get(output)!.element; + prevElement = this.outputElements.get(output)?.element; }); const editorHeight = templateData.editor!.getContentHeight(); @@ -322,13 +324,15 @@ export class CodeCell extends Disposable { const renderedOutput = this.outputElements.get(currOutput); if (renderedOutput) { - if (renderedOutput.renderResult.shadowContent) { - // Show inset in webview, or render output that isn't rendered - this.renderOutput(currOutput, index, undefined); + if (renderedOutput.renderResult.type !== RenderOutputType.None) { + this.notebookEditor.createInset(this.viewCell, renderedOutput.renderResult as IInsetRenderOutput, this.viewCell.getOutputOffset(index)); } else { // Anything else, just update the height this.viewCell.updateOutputHeight(index, renderedOutput.element.clientHeight); } + } else { + // Wasn't previously rendered, render it now + this.renderOutput(currOutput, index); } } @@ -337,6 +341,7 @@ export class CodeCell extends Disposable { private viewUpdateInputCollapsed(): void { DOM.hide(this.templateData.cellContainer); + DOM.hide(this.templateData.runButtonContainer); DOM.show(this.templateData.collapsedPart); DOM.show(this.templateData.outputContainer); this.templateData.container.classList.toggle('collapsed', true); @@ -354,6 +359,7 @@ export class CodeCell extends Disposable { private viewUpdateOutputCollapsed(): void { DOM.show(this.templateData.cellContainer); + DOM.show(this.templateData.runButtonContainer); DOM.show(this.templateData.collapsedPart); DOM.hide(this.templateData.outputContainer); @@ -367,6 +373,7 @@ export class CodeCell extends Disposable { private viewUpdateAllCollapsed(): void { DOM.hide(this.templateData.cellContainer); + DOM.hide(this.templateData.runButtonContainer); DOM.show(this.templateData.collapsedPart); DOM.hide(this.templateData.outputContainer); this.templateData.container.classList.toggle('collapsed', true); @@ -381,6 +388,7 @@ export class CodeCell extends Disposable { private viewUpdateExpanded(): void { DOM.show(this.templateData.cellContainer); + DOM.show(this.templateData.runButtonContainer); DOM.hide(this.templateData.collapsedPart); DOM.show(this.templateData.outputContainer); this.templateData.container.classList.toggle('collapsed', false); @@ -393,7 +401,7 @@ export class CodeCell extends Disposable { private layoutEditor(dimension: IDimension): void { this.templateData.editor?.layout(dimension); - this.templateData.statusBarContainer.style.width = `${dimension.width}px`; + this.templateData.statusBar.layout(dimension.width); } private onCellWidthChange(): void { @@ -408,9 +416,10 @@ export class CodeCell extends Disposable { } ); + // for contents for which we don't observe for dynamic height, update them manually this.viewCell.outputs.forEach((o, i) => { const renderedOutput = this.outputElements.get(o); - if (renderedOutput && !renderedOutput.renderResult.hasDynamicHeight && !renderedOutput.renderResult.shadowContent) { + if (renderedOutput && renderedOutput.renderResult.type === RenderOutputType.None && !renderedOutput.renderResult.hasDynamicHeight) { this.viewCell.updateOutputHeight(i, renderedOutput.element.clientHeight); } }); @@ -428,7 +437,15 @@ export class CodeCell extends Disposable { ); } + private getNotebookUri(): URI | undefined { + return CellUri.parse(this.viewCell.uri)?.notebook; + } + private renderOutput(currOutput: IProcessedOutput, index: number, beforeElement?: HTMLElement) { + if (this.viewCell.metadata.outputCollapsed) { + return; + } + if (!this.outputResizeListeners.has(currOutput)) { this.outputResizeListeners.set(currOutput, new DisposableStore()); } @@ -469,18 +486,21 @@ export class CodeCell extends Disposable { const innerContainer = DOM.$('.output-inner-container'); DOM.append(outputItemDiv, innerContainer); - if (pickedMimeTypeRenderer.isResolved) { - // html - result = this.notebookEditor.getOutputRenderer().render({ outputId: currOutput.outputId, outputKind: CellOutputKind.Rich, data: { 'text/html': pickedMimeTypeRenderer.output! } }, innerContainer, 'text/html'); + + if (pickedMimeTypeRenderer.rendererId !== BUILTIN_RENDERER_ID) { + const renderer = this.notebookService.getRendererInfo(pickedMimeTypeRenderer.rendererId); + result = renderer + ? { type: RenderOutputType.Extension, renderer, source: currOutput, mimeType: pickedMimeTypeRenderer.mimeType } + : this.notebookEditor.getOutputRenderer().render(currOutput, innerContainer, pickedMimeTypeRenderer.mimeType, this.getNotebookUri(),); } else { - result = this.notebookEditor.getOutputRenderer().render(currOutput, innerContainer, pickedMimeTypeRenderer.mimeType); + result = this.notebookEditor.getOutputRenderer().render(currOutput, innerContainer, pickedMimeTypeRenderer.mimeType, this.getNotebookUri(),); } } else { // for text and error, there is no mimetype const innerContainer = DOM.$('.output-inner-container'); DOM.append(outputItemDiv, innerContainer); - result = this.notebookEditor.getOutputRenderer().render(currOutput, innerContainer, undefined); + result = this.notebookEditor.getOutputRenderer().render(currOutput, innerContainer, undefined, this.getNotebookUri(),); } if (!result) { @@ -496,18 +516,16 @@ export class CodeCell extends Disposable { this.templateData.outputContainer?.appendChild(outputItemDiv); } - if (result.shadowContent) { + if (result.type !== RenderOutputType.None) { this.viewCell.selfSizeMonitoring = true; - this.notebookEditor.createInset(this.viewCell, currOutput, result.shadowContent, this.viewCell.getOutputOffset(index)); + this.notebookEditor.createInset(this.viewCell, result as any, this.viewCell.getOutputOffset(index)); } else { DOM.addClass(outputItemDiv, 'foreground'); DOM.addClass(outputItemDiv, 'output-element'); outputItemDiv.style.position = 'absolute'; } - const hasDynamicHeight = result.hasDynamicHeight; - - if (hasDynamicHeight) { + if (outputHasDynamicHeight(result)) { this.viewCell.selfSizeMonitoring = true; const clientHeight = outputItemDiv.clientHeight; @@ -535,18 +553,12 @@ export class CodeCell extends Disposable { elementSizeObserver.startObserving(); this.outputResizeListeners.get(currOutput)!.add(elementSizeObserver); this.viewCell.updateOutputHeight(index, clientHeight); - } else { - if (result.shadowContent) { - // webview - // noop - } else { - // static output - const clientHeight = Math.ceil(outputItemDiv.clientHeight); - this.viewCell.updateOutputHeight(index, clientHeight); + } else if (result.type === RenderOutputType.None) { // no-op if it's a webview + const clientHeight = Math.ceil(outputItemDiv.clientHeight); + this.viewCell.updateOutputHeight(index, clientHeight); - const top = this.viewCell.getOutputOffsetInContainer(index); - outputItemDiv.style.top = `${top}px`; - } + const top = this.viewCell.getOutputOffsetInContainer(index); + outputItemDiv.style.top = `${top}px`; } } @@ -558,7 +570,8 @@ export class CodeCell extends Disposable { const renderInfo = this.notebookService.getRendererInfo(renderId); if (renderInfo) { - return `${renderId} (${renderInfo.extensionId.value})`; + const displayName = renderInfo.displayName !== '' ? renderInfo.displayName : renderInfo.id; + return `${displayName} (${renderInfo.extensionId.value})`; } return nls.localize('builtinRenderInfo', "built-in"); @@ -604,17 +617,6 @@ export class CodeCell extends Disposable { } output.pickedMimeTypeIndex = pick; - - if (!output.orderedMimeTypes![pick].isResolved && output.orderedMimeTypes![pick].rendererId !== BUILTIN_RENDERER_ID) { - // since it's not build in renderer and not resolved yet - // let's see if we can activate the extension and then render - // await this.notebookService.transformSpliceOutputs(this.notebookEditor.textModel!, [[0, 0, output]]) - const outputRet = await this.notebookService.transformSingleOutput(this.notebookEditor.textModel!, output, output.orderedMimeTypes![pick].rendererId!, output.orderedMimeTypes![pick].mimeType); - if (outputRet) { - output.orderedMimeTypes![pick] = outputRet; - } - } - this.renderOutput(output, index, nextElement); this.relayoutCell(); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents.ts new file mode 100644 index 00000000000..989f392ea4c --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents.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 { renderCodicons } from 'vs/base/common/codicons'; +import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { INotificationService } from 'vs/platform/notification/common/notification'; + +export class CodiconActionViewItem extends MenuEntryActionViewItem { + constructor( + readonly _action: MenuItemAction, + keybindingService: IKeybindingService, + notificationService: INotificationService, + contextMenuService: IContextMenuService + ) { + super(_action, keybindingService, notificationService, contextMenuService); + } + updateLabel(): void { + if (this.options.label && this.label) { + this.label.innerHTML = renderCodicons(this._commandAction.label ?? ''); + } + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/dnd.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/dnd.ts new file mode 100644 index 00000000000..7f5e4ad30a0 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/dnd.ts @@ -0,0 +1,278 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import * as DOM from 'vs/base/browser/dom'; +import { domEvent } from 'vs/base/browser/event'; +import { Delayer } from 'vs/base/common/async'; +import { Disposable } from 'vs/base/common/lifecycle'; +import * as platform from 'vs/base/common/platform'; +import { BOTTOM_CELL_TOOLBAR_GAP } from 'vs/workbench/contrib/notebook/browser/constants'; +import { BaseCellRenderTemplate, CellEditState, ICellViewModel, INotebookCellList, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +const $ = DOM.$; + +export const DRAGGING_CLASS = 'cell-dragging'; +export const GLOBAL_DRAG_CLASS = 'global-drag-active'; + +type DragImageProvider = () => HTMLElement; + +interface CellDragEvent { + browserEvent: DragEvent; + draggedOverCell: ICellViewModel; + cellTop: number; + cellHeight: number; + dragPosRatio: number; +} + +export class CellDragAndDropController extends Disposable { + // TODO@roblourens - should probably use dataTransfer here, but any dataTransfer set makes the editor think I am dropping a file, need + // to figure out how to prevent that + private currentDraggedCell: ICellViewModel | undefined; + + private listInsertionIndicator: HTMLElement; + + private list!: INotebookCellList; + + private isScrolling = false; + private scrollingDelayer: Delayer; + + constructor( + private readonly notebookEditor: INotebookEditor, + insertionIndicatorContainer: HTMLElement + ) { + super(); + + this.listInsertionIndicator = DOM.append(insertionIndicatorContainer, $('.cell-list-insertion-indicator')); + + this._register(domEvent(document.body, DOM.EventType.DRAG_START, true)(this.onGlobalDragStart.bind(this))); + this._register(domEvent(document.body, DOM.EventType.DRAG_END, true)(this.onGlobalDragEnd.bind(this))); + + const addCellDragListener = (eventType: string, handler: (e: CellDragEvent) => void) => { + this._register(DOM.addDisposableListener( + notebookEditor.getDomNode(), + eventType, + e => { + const cellDragEvent = this.toCellDragEvent(e); + if (cellDragEvent) { + handler(cellDragEvent); + } + })); + }; + + addCellDragListener(DOM.EventType.DRAG_OVER, event => { + event.browserEvent.preventDefault(); + this.onCellDragover(event); + }); + addCellDragListener(DOM.EventType.DROP, event => { + event.browserEvent.preventDefault(); + this.onCellDrop(event); + }); + addCellDragListener(DOM.EventType.DRAG_LEAVE, event => { + event.browserEvent.preventDefault(); + this.onCellDragLeave(event); + }); + + this.scrollingDelayer = new Delayer(200); + } + + setList(value: INotebookCellList) { + this.list = value; + + this.list.onWillScroll(e => { + if (!e.scrollTopChanged) { + return; + } + + this.setInsertIndicatorVisibility(false); + this.isScrolling = true; + this.scrollingDelayer.trigger(() => { + this.isScrolling = false; + }); + }); + } + + private setInsertIndicatorVisibility(visible: boolean) { + this.listInsertionIndicator.style.opacity = visible ? '1' : '0'; + } + + private toCellDragEvent(event: DragEvent): CellDragEvent | undefined { + const targetTop = this.notebookEditor.getDomNode().getBoundingClientRect().top; + const dragOffset = this.list.scrollTop + event.clientY - targetTop; + const draggedOverCell = this.list.elementAt(dragOffset); + if (!draggedOverCell) { + return undefined; + } + + const cellTop = this.list.getAbsoluteTopOfElement(draggedOverCell); + const cellHeight = this.list.elementHeight(draggedOverCell); + + const dragPosInElement = dragOffset - cellTop; + const dragPosRatio = dragPosInElement / cellHeight; + + return { + browserEvent: event, + draggedOverCell, + cellTop, + cellHeight, + dragPosRatio + }; + } + + clearGlobalDragState() { + this.notebookEditor.getDomNode().classList.remove(GLOBAL_DRAG_CLASS); + } + + private onGlobalDragStart() { + this.notebookEditor.getDomNode().classList.add(GLOBAL_DRAG_CLASS); + } + + private onGlobalDragEnd() { + this.notebookEditor.getDomNode().classList.remove(GLOBAL_DRAG_CLASS); + } + + private onCellDragover(event: CellDragEvent): void { + if (!event.browserEvent.dataTransfer) { + return; + } + + if (!this.currentDraggedCell) { + event.browserEvent.dataTransfer.dropEffect = 'none'; + return; + } + + if (this.isScrolling || this.currentDraggedCell === event.draggedOverCell) { + this.setInsertIndicatorVisibility(false); + return; + } + + const dropDirection = this.getDropInsertDirection(event); + const insertionIndicatorAbsolutePos = dropDirection === 'above' ? event.cellTop : event.cellTop + event.cellHeight; + const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop + BOTTOM_CELL_TOOLBAR_GAP / 2; + if (insertionIndicatorTop >= 0) { + this.listInsertionIndicator.style.top = `${insertionIndicatorTop}px`; + this.setInsertIndicatorVisibility(true); + } else { + this.setInsertIndicatorVisibility(false); + } + } + + private getDropInsertDirection(event: CellDragEvent): 'above' | 'below' { + return event.dragPosRatio < 0.5 ? 'above' : 'below'; + } + + private onCellDrop(event: CellDragEvent): void { + const draggedCell = this.currentDraggedCell!; + + if (this.isScrolling || this.currentDraggedCell === event.draggedOverCell) { + return; + } + + let draggedCells: ICellViewModel[] = [draggedCell]; + let draggedCellRange: [number, number] = [this.notebookEditor.viewModel!.getCellIndex(draggedCell), 1]; + + if (draggedCell.cellKind === CellKind.Markdown) { + const currCellIndex = this.notebookEditor.viewModel!.getCellIndex(draggedCell); + const nextVisibleCellIndex = this.notebookEditor.viewModel!.getNextVisibleCellIndex(currCellIndex); + + if (nextVisibleCellIndex > currCellIndex + 1) { + // folding ;) + draggedCells = this.notebookEditor.viewModel!.viewCells.slice(currCellIndex, nextVisibleCellIndex); + draggedCellRange = [currCellIndex, nextVisibleCellIndex - currCellIndex]; + } + } + + this.dragCleanup(); + + const isCopy = (event.browserEvent.ctrlKey && !platform.isMacintosh) || (event.browserEvent.altKey && platform.isMacintosh); + + const dropDirection = this.getDropInsertDirection(event); + const insertionIndicatorAbsolutePos = dropDirection === 'above' ? event.cellTop : event.cellTop + event.cellHeight; + const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop + BOTTOM_CELL_TOOLBAR_GAP / 2; + const editorHeight = this.notebookEditor.getDomNode().getBoundingClientRect().height; + if (insertionIndicatorTop < 0 || insertionIndicatorTop > editorHeight) { + // Ignore drop, insertion point is off-screen + return; + } + + if (isCopy) { + this.copyCells(draggedCells, event.draggedOverCell, dropDirection); + } else { + const viewModel = this.notebookEditor.viewModel!; + let originalToIdx = viewModel.getCellIndex(event.draggedOverCell); + if (dropDirection === 'below') { + const relativeToIndex = viewModel.getCellIndex(event.draggedOverCell); + const newIdx = viewModel.getNextVisibleCellIndex(relativeToIndex); + originalToIdx = newIdx; + } + + this.notebookEditor.moveCellsToIdx(draggedCellRange[0], draggedCellRange[1], originalToIdx); + } + } + + private onCellDragLeave(event: CellDragEvent): void { + if (!event.browserEvent.relatedTarget || !DOM.isAncestor(event.browserEvent.relatedTarget as HTMLElement, this.notebookEditor.getDomNode())) { + this.setInsertIndicatorVisibility(false); + } + } + + private dragCleanup(): void { + if (this.currentDraggedCell) { + this.currentDraggedCell.dragging = false; + this.currentDraggedCell = undefined; + } + + this.setInsertIndicatorVisibility(false); + } + + registerDragHandle(templateData: BaseCellRenderTemplate, cellRoot: HTMLElement, dragHandle: HTMLElement, dragImageProvider: DragImageProvider): void { + const container = templateData.container; + dragHandle.setAttribute('draggable', 'true'); + + templateData.disposables.add(domEvent(dragHandle, DOM.EventType.DRAG_END)(() => { + // Note, templateData may have a different element rendered into it by now + container.classList.remove(DRAGGING_CLASS); + this.dragCleanup(); + })); + + templateData.disposables.add(domEvent(dragHandle, DOM.EventType.DRAG_START)(event => { + if (!event.dataTransfer) { + return; + } + + this.currentDraggedCell = templateData.currentRenderedCell!; + this.currentDraggedCell.dragging = true; + + const dragImage = dragImageProvider(); + cellRoot.parentElement!.appendChild(dragImage); + event.dataTransfer.setDragImage(dragImage, 0, 0); + setTimeout(() => cellRoot.parentElement!.removeChild(dragImage!), 0); // Comment this out to debug drag image layout + + container.classList.add(DRAGGING_CLASS); + })); + } + + private copyCells(draggedCells: ICellViewModel[], ontoCell: ICellViewModel, direction: 'above' | 'below') { + this.notebookEditor.textModel!.pushStackElement('Copy Cells'); + let firstNewCell: ICellViewModel | undefined = undefined; + let firstNewCellState: CellEditState = CellEditState.Preview; + for (let i = 0; i < draggedCells.length; i++) { + const draggedCell = draggedCells[i]; + const newCell = this.notebookEditor.insertNotebookCell(ontoCell, draggedCell.cellKind, direction, draggedCell.getText()); + + if (newCell && !firstNewCell) { + firstNewCell = newCell; + firstNewCellState = draggedCell.editState; + } + } + + if (firstNewCell) { + this.notebookEditor.focusNotebookCell(firstNewCell, firstNewCellState === CellEditState.Editing ? 'editor' : 'container'); + } + + this.notebookEditor.textModel!.pushStackElement('Copy Cells'); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts index 461c388c8d4..a0fea876cd3 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -177,7 +177,7 @@ export class StatefulMarkdownCell extends Disposable { const lineHeight = this.viewCell.layoutInfo.fontInfo?.lineHeight || 17; editorHeight = Math.max(lineNum, 1) * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; - this.templateData.editorContainer.innerHTML = ''; + this.templateData.editorContainer.innerText = ''; // create a special context key service that set the inCompositeEditor-contextkey const editorContextKeyService = this.contextKeyService.createScoped(); @@ -190,7 +190,7 @@ export class StatefulMarkdownCell extends Disposable { width: width, height: editorHeight }, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() + // overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() }, {}); this.templateData.currentEditor = this.editor; @@ -241,7 +241,7 @@ export class StatefulMarkdownCell extends Disposable { this.renderedEditors.delete(this.viewCell); - this.markdownContainer.innerHTML = ''; + this.markdownContainer.innerText = ''; this.viewCell.clearHTML(); const markdownRenderer = this.viewCell.getMarkdownRenderer(); const renderedHTML = this.viewCell.getHTML(); @@ -261,7 +261,7 @@ export class StatefulMarkdownCell extends Disposable { })); this.localDisposables.add(this.viewCell.textBuffer.onDidChangeContent(() => { - this.markdownContainer.innerHTML = ''; + this.markdownContainer.innerText = ''; this.viewCell.clearHTML(); const renderedHTML = this.viewCell.getHTML(); if (renderedHTML) { @@ -282,7 +282,7 @@ export class StatefulMarkdownCell extends Disposable { private layoutEditor(dimension: DOM.IDimension): void { this.editor?.layout(dimension); - this.templateData.statusBarContainer.style.width = `${dimension.width}px`; + this.templateData.statusBar.layout(dimension.width); } private onCellEditorWidthChange(): void { @@ -312,7 +312,7 @@ export class StatefulMarkdownCell extends Disposable { setFoldingIndicator() { switch (this.foldingState) { case CellFoldingState.None: - this.templateData.foldingIndicator.innerHTML = ''; + this.templateData.foldingIndicator.innerText = ''; break; case CellFoldingState.Collapsed: this.templateData.foldingIndicator.innerHTML = renderCodicons('$(chevron-right)'); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 7cb5b349f20..9caa12e52b7 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -6,6 +6,7 @@ import type { Event } from 'vs/base/common/event'; import type { IDisposable } from 'vs/base/common/lifecycle'; import { ToWebviewMessage } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; +import { RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; // !! IMPORTANT !! everything must be in-line within the webviewPreloads // function. Imports are not allowed. This is stringifies and injected into @@ -373,24 +374,21 @@ function webviewPreloads() { addMouseoverListeners(outputNode, outputId); const content = data.content; - outputNode.innerHTML = content; - cellOutputContainer.appendChild(outputNode); - - let pureData: { mimeType: string, output: unknown } | undefined; - const outputScript = cellOutputContainer.querySelector('script.vscode-pure-data'); - if (outputScript) { - try { pureData = JSON.parse(outputScript.innerHTML); } catch { } + if (content.type === RenderOutputType.Html) { + outputNode.innerHTML = content.htmlContent; + cellOutputContainer.appendChild(outputNode); + domEval(outputNode); + } else { + onDidCreateOutput.fire([data.apiNamespace, { + element: outputNode, + output: content.output, + mimeType: content.mimeType, + outputId + }]); + cellOutputContainer.appendChild(outputNode); } - // eval - domEval(outputNode); resizeObserve(outputNode, outputId); - onDidCreateOutput.fire([data.apiNamespace, { - element: outputNode, - output: pureData?.output, - mimeType: pureData?.mimeType, - outputId - }]); vscode.postMessage({ __vscode_notebook_message: true, @@ -422,7 +420,7 @@ function webviewPreloads() { case 'clear': queuedOuputActions.clear(); // stop all loading outputs onWillDestroyOutput.fire([undefined, undefined]); - document.getElementById('container')!.innerHTML = ''; + document.getElementById('container')!.innerText = ''; outputObservers.forEach(ob => { ob.disconnect(); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index d70f6e4c842..d145744d489 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -12,12 +12,14 @@ import { IPosition } from 'vs/editor/common/core/position'; import * as editorCommon from 'vs/editor/common/editorCommon'; import * as model from 'vs/editor/common/model'; import { SearchParams } from 'vs/editor/common/model/textModelSearch'; -import { EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CELL_STATUSBAR_HEIGHT, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; import { CellEditState, CellFocusMode, CursorAtBoundary, CellViewModelStateChangeEvent, IEditableCellViewModel, INotebookCellDecorationOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellKind, NotebookCellMetadata, NotebookDocumentMetadata, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, NotebookDocumentMetadata, INotebookSearchOptions, ShowCellStatusbarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export abstract class BaseCellViewModel extends Disposable { + protected readonly _onDidChangeEditorAttachState = new Emitter(); // Do not merge this event with `onDidChangeState` as we are using `Event.once(onDidChangeEditorAttachState)` elsewhere. readonly onDidChangeEditorAttachState = this._onDidChangeEditorAttachState.event; @@ -106,7 +108,12 @@ export abstract class BaseCellViewModel extends Disposable { this._dragging = v; } - constructor(readonly viewType: string, readonly model: NotebookCellTextModel, public id: string) { + constructor( + readonly viewType: string, + readonly model: NotebookCellTextModel, + public id: string, + private readonly _configurationService: IConfigurationService + ) { super(); this._register(model.onDidChangeLanguage(() => { @@ -116,12 +123,24 @@ export abstract class BaseCellViewModel extends Disposable { this._register(model.onDidChangeMetadata(() => { this._onDidChangeState.fire({ metadataChanged: true }); })); + + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(ShowCellStatusbarKey)) { + this.layoutChange({}); + } + })); + } + + protected getEditorStatusbarHeight() { + const showCellStatusBar = this._configurationService.getValue(ShowCellStatusbarKey); + return showCellStatusBar ? CELL_STATUSBAR_HEIGHT : 0; } // abstract resolveTextModel(): Promise; abstract hasDynamicHeight(): boolean; abstract getHeight(lineHeight: number): number; abstract onDeselect(): void; + abstract layoutChange(change: any): void; assertTextModelAttached(): boolean { if (this.textModel && this._textEditor && this._textEditor.getModel() === this.textModel) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index 306c9722c8d..0a7c21cf9be 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -8,12 +8,13 @@ import * as UUID from 'vs/base/common/uuid'; import * as editorCommon from 'vs/editor/common/editorCommon'; import * as model from 'vs/editor/common/model'; import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; -import { BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_MARGIN, CELL_RUN_GUTTER, CELL_STATUSBAR_HEIGHT, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, CELL_TOP_MARGIN, EDITOR_TOP_PADDING, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN, BOTTOM_CELL_TOOLBAR_OFFSET, COLLAPSED_INDICATOR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFindMatch, CodeCellLayoutChangeEvent, CodeCellLayoutInfo, ICellViewModel, NotebookLayoutInfo, CodeCellLayoutState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, NotebookCellOutputsSplice, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { BaseCellViewModel } from './baseCellViewModel'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_RUN_GUTTER, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CellEditState, CellFindMatch, CodeCellLayoutChangeEvent, CodeCellLayoutInfo, CodeCellLayoutState, ICellViewModel, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { CellKind, INotebookSearchOptions, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { BaseCellViewModel } from './baseCellViewModel'; export class CodeCellViewModel extends BaseCellViewModel implements ICellViewModel { readonly cellKind = CellKind.Code; @@ -68,9 +69,10 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod readonly viewType: string, readonly model: NotebookCellTextModel, initialNotebookLayoutInfo: NotebookLayoutInfo | null, - readonly eventDispatcher: NotebookEventDispatcher + readonly eventDispatcher: NotebookEventDispatcher, + @IConfigurationService configurationService: IConfigurationService ) { - super(viewType, model, UUID.generateUuid()); + super(viewType, model, UUID.generateUuid(), configurationService); this._register(this.model.onDidChangeOutputs((splices) => { this._outputCollection = new Array(this.model.outputs.length); this._outputsTop = null; @@ -121,9 +123,10 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod newState = CodeCellLayoutState.Estimated; } - const indicatorHeight = editorHeight + CELL_STATUSBAR_HEIGHT + outputTotalHeight; - const outputContainerOffset = EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN + editorHeight + CELL_STATUSBAR_HEIGHT; - const bottomToolbarOffset = totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT - BOTTOM_CELL_TOOLBAR_OFFSET; + const statusbarHeight = this.getEditorStatusbarHeight(); + const indicatorHeight = editorHeight + statusbarHeight + outputTotalHeight; + const outputContainerOffset = EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN + editorHeight + statusbarHeight; + const bottomToolbarOffset = totalHeight - BOTTOM_CELL_TOOLBAR_GAP - BOTTOM_CELL_TOOLBAR_HEIGHT / 2; const editorWidth = state.outerWidth !== undefined ? this.computeEditorWidth(state.outerWidth) : this._layoutInfo?.editorWidth; this._layoutInfo = { @@ -141,8 +144,8 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod outputTotalHeight = this.metadata?.inputCollapsed && this.metadata.outputCollapsed ? 0 : outputTotalHeight; const indicatorHeight = COLLAPSED_INDICATOR_HEIGHT + outputTotalHeight; const outputContainerOffset = CELL_TOP_MARGIN + COLLAPSED_INDICATOR_HEIGHT; - const totalHeight = CELL_TOP_MARGIN + COLLAPSED_INDICATOR_HEIGHT + CELL_BOTTOM_MARGIN + BOTTOM_CELL_TOOLBAR_HEIGHT + outputTotalHeight; - const bottomToolbarOffset = totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT - BOTTOM_CELL_TOOLBAR_OFFSET; + const totalHeight = CELL_TOP_MARGIN + COLLAPSED_INDICATOR_HEIGHT + CELL_BOTTOM_MARGIN + BOTTOM_CELL_TOOLBAR_GAP + outputTotalHeight; + const bottomToolbarOffset = totalHeight - BOTTOM_CELL_TOOLBAR_GAP - BOTTOM_CELL_TOOLBAR_HEIGHT / 2; const editorWidth = state.outerWidth !== undefined ? this.computeEditorWidth(state.outerWidth) : this._layoutInfo?.editorWidth; this._layoutInfo = { @@ -209,7 +212,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod } private computeTotalHeight(editorHeight: number, outputsTotalHeight: number): number { - return EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN + editorHeight + CELL_STATUSBAR_HEIGHT + outputsTotalHeight + BOTTOM_CELL_TOOLBAR_HEIGHT + CELL_BOTTOM_MARGIN; + return EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN + editorHeight + this.getEditorStatusbarHeight() + outputsTotalHeight + BOTTOM_CELL_TOOLBAR_GAP + CELL_BOTTOM_MARGIN; } /** diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts index 8700ba68862..bca810545ec 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts @@ -70,3 +70,24 @@ export class NotebookEventDispatcher { } } } + +export class NotebookDiffEditorEventDispatcher { + protected readonly _onDidChangeLayout = new Emitter(); + readonly onDidChangeLayout = this._onDidChangeLayout.event; + + constructor() { + } + + emit(events: NotebookViewEvent[]) { + for (let i = 0, len = events.length; i < len; i++) { + const e = events[i]; + + switch (e.type) { + case NotebookViewEventType.LayoutChanged: + this._onDidChangeLayout.fire(e); + break; + } + } + } + +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts index 9c1dc125163..2916dcd868e 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts @@ -3,19 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { Emitter, Event } from 'vs/base/common/event'; import * as UUID from 'vs/base/common/uuid'; import * as editorCommon from 'vs/editor/common/editorCommon'; import * as model from 'vs/editor/common/model'; -import { BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_MARGIN, CELL_STATUSBAR_HEIGHT, CELL_TOP_MARGIN, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN, BOTTOM_CELL_TOOLBAR_OFFSET, COLLAPSED_INDICATOR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; +import * as nls from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; +import { EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; import { CellFindMatch, ICellViewModel, MarkdownCellLayoutChangeEvent, MarkdownCellLayoutInfo, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer'; import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel'; -import { EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; +import { NotebookCellStateChangedEvent, NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookEventDispatcher, NotebookCellStateChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; export class MarkdownCellViewModel extends BaseCellViewModel implements ICellViewModel { readonly cellKind = CellKind.Markdown; @@ -27,7 +28,7 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie } set renderedMarkdownHeight(newHeight: number) { - const newTotalHeight = newHeight + BOTTOM_CELL_TOOLBAR_HEIGHT; + const newTotalHeight = newHeight + BOTTOM_CELL_TOOLBAR_GAP; this.totalHeight = newTotalHeight; } @@ -45,7 +46,7 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie set editorHeight(newHeight: number) { this._editorHeight = newHeight; - this.totalHeight = this._editorHeight + CELL_TOP_MARGIN + CELL_BOTTOM_MARGIN + BOTTOM_CELL_TOOLBAR_HEIGHT + CELL_STATUSBAR_HEIGHT; + this.totalHeight = this._editorHeight + CELL_TOP_MARGIN + CELL_BOTTOM_MARGIN + BOTTOM_CELL_TOOLBAR_GAP + this.getEditorStatusbarHeight(); } get editorHeight() { @@ -65,15 +66,16 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie initialNotebookLayoutInfo: NotebookLayoutInfo | null, readonly foldingDelegate: EditorFoldingStateDelegate, readonly eventDispatcher: NotebookEventDispatcher, - private readonly _mdRenderer: MarkdownRenderer + private readonly _mdRenderer: MarkdownRenderer, + @IConfigurationService configurationService: IConfigurationService ) { - super(viewType, model, UUID.generateUuid()); + super(viewType, model, UUID.generateUuid(), configurationService); this._layoutInfo = { editorHeight: 0, fontInfo: initialNotebookLayoutInfo?.fontInfo || null, editorWidth: initialNotebookLayoutInfo?.width ? this.computeEditorWidth(initialNotebookLayoutInfo.width) : 0, - bottomToolbarOffset: BOTTOM_CELL_TOOLBAR_HEIGHT, + bottomToolbarOffset: BOTTOM_CELL_TOOLBAR_GAP, totalHeight: 0 }; @@ -101,19 +103,19 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie fontInfo: state.font || this._layoutInfo.fontInfo, editorWidth, editorHeight: this._editorHeight, - bottomToolbarOffset: totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT - BOTTOM_CELL_TOOLBAR_OFFSET, + bottomToolbarOffset: totalHeight - BOTTOM_CELL_TOOLBAR_GAP - BOTTOM_CELL_TOOLBAR_HEIGHT / 2, totalHeight }; } else { const editorWidth = state.outerWidth !== undefined ? this.computeEditorWidth(state.outerWidth) : this._layoutInfo.editorWidth; - const totalHeight = CELL_TOP_MARGIN + COLLAPSED_INDICATOR_HEIGHT + BOTTOM_CELL_TOOLBAR_HEIGHT + CELL_BOTTOM_MARGIN; + const totalHeight = CELL_TOP_MARGIN + COLLAPSED_INDICATOR_HEIGHT + BOTTOM_CELL_TOOLBAR_GAP + CELL_BOTTOM_MARGIN; state.totalHeight = totalHeight; this._layoutInfo = { fontInfo: state.font || this._layoutInfo.fontInfo, editorWidth, editorHeight: this._editorHeight, - bottomToolbarOffset: totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT - BOTTOM_CELL_TOOLBAR_OFFSET, + bottomToolbarOffset: totalHeight - BOTTOM_CELL_TOOLBAR_GAP - BOTTOM_CELL_TOOLBAR_HEIGHT / 2, totalHeight }; } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index 47739daac44..44d4d664aac 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -8,7 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditService, ResourceEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness, IReadonlyTextBuffer } from 'vs/editor/common/model'; @@ -17,13 +17,13 @@ import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { WorkspaceTextEdit } from 'vs/editor/common/modes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { CellEditState, CellFindMatch, ICellRange, ICellViewModel, NotebookLayoutInfo, IEditableCellViewModel, INotebookDeltaDecoration } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFindMatch, ICellViewModel, NotebookLayoutInfo, IEditableCellViewModel, INotebookDeltaDecoration } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { NotebookEventDispatcher, NotebookMetadataChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { CellFoldingState, EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, NotebookCellMetadata, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, INotebookSearchOptions, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer'; @@ -170,10 +170,6 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return this._notebook; } - get renderers() { - return this._notebook!.renderers; - } - get handle() { return this._notebook.handle; } @@ -349,16 +345,6 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD }); } - inspectLayout() { - console.log('--- notebook ---\n'); - console.log(this.layoutInfo); - console.log('--- cells ---'); - this.viewCells.forEach(cell => { - console.log(`--- cell: ${cell.handle} ---\n`); - console.log((cell as (CodeCellViewModel | MarkdownCellViewModel)).layoutInfo); - }); - } - setFocus(focused: boolean) { this._focused = focused; } @@ -623,8 +609,9 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return result; } - createCell(index: number, source: string | string[], language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, synchronous: boolean, pushUndoStop: boolean = true) { - this._notebook.createCell2(index, source, language, type, metadata, synchronous, pushUndoStop, undefined, undefined); + createCell(index: number, source: string, language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, synchronous: boolean, pushUndoStop: boolean = true, previouslyFocused: ICellViewModel[] = []) { + const beforeSelections = previouslyFocused.map(e => e.handle); + this._notebook.createCell2(index, source, language, type, metadata, synchronous, pushUndoStop, beforeSelections, undefined); // TODO, rely on createCell to be sync return this.viewCells[index]; } @@ -657,13 +644,21 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._notebook.deleteCell2(index, synchronous, pushUndoStop, this.selectionHandles, endSelections); } - moveCellToIdx(index: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean = true): boolean { + /** + * + * @param index + * @param length + * @param newIdx in an index scheme for the state of the tree after the current cell has been "removed" + * @param synchronous + * @param pushedToUndoStack + */ + moveCellToIdx(index: number, length: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean = true): boolean { const viewCell = this.viewCells[index] as CellViewModel; if (!viewCell) { return false; } - this._notebook.moveCellToIdx2(index, newIdx, synchronous, pushedToUndoStack, undefined, [viewCell.handle]); + this._notebook.moveCellToIdx2(index, length, newIdx, synchronous, pushedToUndoStack, undefined, [viewCell.handle]); return true; } @@ -761,7 +756,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD language, kind, { - createCell: (index: number, source: string | string[], language: string, type: CellKind) => { + createCell: (index: number, source: string, language: string, type: CellKind) => { return this.createCell(index, source, language, type, undefined, true, false) as BaseCellViewModel; }, deleteCell: (index: number) => { @@ -1033,7 +1028,10 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD const viewCell = cell as CellViewModel; this._lastNotebookEditResource.push(viewCell.uri); return viewCell.resolveTextModel().then(() => { - this._bulkEditService.apply({ edits: [{ edit: { range: range, text: text }, resource: cell.uri }] }, { quotableLabel: 'Notebook Replace' }); + this._bulkEditService.apply( + [new ResourceTextEdit(cell.uri, { range, text })], + { quotableLabel: 'Notebook Replace' } + ); }); } @@ -1057,7 +1055,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return Promise.all(matches.map(match => { return match.cell.resolveTextModel(); })).then(async () => { - this._bulkEditService.apply({ edits: textEdits }, { quotableLabel: 'Notebook Replace All' }); + this._bulkEditService.apply(ResourceEdit.convert({ edits: textEdits }), { quotableLabel: 'Notebook Replace All' }); return; }); } @@ -1078,12 +1076,15 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD const element = editStack.past.length ? editStack.past[editStack.past.length - 1] : undefined; if (element && element instanceof SingleModelEditStackElement || element instanceof MultiModelEditStackElement) { - return await this.withElement(element, async () => { + await this.withElement(element, async () => { await this._undoService.undo(this.uri); }); + + return (element instanceof SingleModelEditStackElement) ? [element.resource] : element.resources; } await this._undoService.undo(this.uri); + return []; } async redo() { @@ -1095,13 +1096,16 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD const element = editStack.future[0]; if (element && element instanceof SingleModelEditStackElement || element instanceof MultiModelEditStackElement) { - return await this.withElement(element, async () => { + await this.withElement(element, async () => { await this._undoService.redo(this.uri); }); + + return (element instanceof SingleModelEditStackElement) ? [element.resource] : element.resources; } await this._undoService.redo(this.uri); + return []; } equal(notebook: NotebookTextModel) { diff --git a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts index 0d609102629..5cde6b3bfaa 100644 --- a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts +++ b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts @@ -6,6 +6,7 @@ import { IResourceUndoRedoElement, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; /** * It should not modify Undo/Redo stack @@ -13,7 +14,8 @@ import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/mode export interface ITextCellEditingDelegate { insertCell?(index: number, cell: NotebookCellTextModel): void; deleteCell?(index: number): void; - moveCell?(fromIndex: number, toIndex: number, beforeSelections: number[] | undefined, endSelections: number[] | undefined): void; + moveCell?(fromIndex: number, length: number, toIndex: number, beforeSelections: number[] | undefined, endSelections: number[] | undefined): void; + updateCellMetadata?(index: number, newMetadata: NotebookCellMetadata): void; emitSelections(selections: number[]): void; } @@ -100,6 +102,7 @@ export class MoveCellEdit implements IResourceUndoRedoElement { constructor( public resource: URI, private fromIndex: number, + private length: number, private toIndex: number, private editingDelegate: ITextCellEditingDelegate, private beforedSelections: number[] | undefined, @@ -112,7 +115,7 @@ export class MoveCellEdit implements IResourceUndoRedoElement { throw new Error('Notebook Move Cell not implemented for Undo/Redo'); } - this.editingDelegate.moveCell(this.toIndex, this.fromIndex, this.endSelections, this.beforedSelections); + this.editingDelegate.moveCell(this.toIndex, this.length, this.fromIndex, this.endSelections, this.beforedSelections); if (this.beforedSelections) { this.editingDelegate.emitSelections(this.beforedSelections); } @@ -123,7 +126,7 @@ export class MoveCellEdit implements IResourceUndoRedoElement { throw new Error('Notebook Move Cell not implemented for Undo/Redo'); } - this.editingDelegate.moveCell(this.fromIndex, this.toIndex, this.beforedSelections, this.endSelections); + this.editingDelegate.moveCell(this.fromIndex, this.length, this.toIndex, this.beforedSelections, this.endSelections); if (this.endSelections) { this.editingDelegate.emitSelections(this.endSelections); } @@ -182,3 +185,33 @@ export class SpliceCellsEdit implements IResourceUndoRedoElement { } } } + +export class CellMetadataEdit implements IResourceUndoRedoElement { + type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; + label: string = 'Update Cell Metadata'; + constructor( + public resource: URI, + readonly index: number, + readonly oldMetadata: NotebookCellMetadata, + readonly newMetadata: NotebookCellMetadata, + private editingDelegate: ITextCellEditingDelegate, + ) { + + } + + undo(): void { + if (!this.editingDelegate.updateCellMetadata) { + return; + } + + this.editingDelegate.updateCellMetadata(this.index, this.oldMetadata); + } + + redo(): void | Promise { + if (!this.editingDelegate.updateCellMetadata) { + return; + } + + this.editingDelegate.updateCellMetadata(this.index, this.newMetadata); + } +} diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index 015f62a96e3..c2b34adefcf 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { ICell, IProcessedOutput, NotebookCellOutputsSplice, CellKind, NotebookCellMetadata, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICell, IProcessedOutput, NotebookCellOutputsSplice, CellKind, NotebookCellMetadata, NotebookDocumentMetadata, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; import { URI } from 'vs/base/common/uri'; import * as model from 'vs/editor/common/model'; import { Range } from 'vs/editor/common/core/range'; import { Disposable } from 'vs/base/common/lifecycle'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { hash } from 'vs/base/common/hash'; export class NotebookCellTextModel extends Disposable implements ICell { private _onDidChangeOutputs = new Emitter(); @@ -31,14 +32,15 @@ export class NotebookCellTextModel extends Disposable implements ICell { return this._outputs; } - private _metadata: NotebookCellMetadata | undefined; + private _metadata: NotebookCellMetadata; get metadata() { return this._metadata; } - set metadata(newMetadata: NotebookCellMetadata | undefined) { + set metadata(newMetadata: NotebookCellMetadata) { this._metadata = newMetadata; + this._hash = null; this._onDidChangeMetadata.fire(); } @@ -48,6 +50,7 @@ export class NotebookCellTextModel extends Disposable implements ICell { set language(newLanguage: string) { this._language = newLanguage; + this._hash = null; this._onDidChangeLanguage.fire(newLanguage); } @@ -59,31 +62,35 @@ export class NotebookCellTextModel extends Disposable implements ICell { } const builder = new PieceTreeTextBufferBuilder(); - builder.acceptChunk(Array.isArray(this._source) ? this._source.join('\n') : this._source); + builder.acceptChunk(this._source); const bufferFactory = builder.finish(true); this._textBuffer = bufferFactory.create(model.DefaultEndOfLine.LF); this._register(this._textBuffer.onDidChangeContent(() => { + this._hash = null; this._onDidChangeContent.fire(); })); return this._textBuffer; } + private _hash: number | null = null; + constructor( readonly uri: URI, public handle: number, - private _source: string | string[], + private _source: string, private _language: string, public cellKind: CellKind, outputs: IProcessedOutput[], metadata: NotebookCellMetadata | undefined, + public readonly transientOptions: TransientOptions, private readonly _modelService: ITextModelService ) { super(); this._outputs = outputs; - this._metadata = metadata; + this._metadata = metadata || {}; } getValue(): string { @@ -96,6 +103,31 @@ export class NotebookCellTextModel extends Disposable implements ICell { } } + getHashValue(): number { + if (this._hash !== null) { + return this._hash; + } + + // TODO, raw outputs + this._hash = hash([hash(this.getValue()), this._getPersisentMetadata, this.transientOptions.transientOutputs ? [] : this._outputs]); + return this._hash; + } + + private _getPersisentMetadata() { + let filteredMetadata: { [key: string]: any } = {}; + const transientMetadata = this.transientOptions.transientMetadata; + + const keys = new Set([...Object.keys(this.metadata)]); + for (let key of keys) { + if (!(transientMetadata[key as keyof NotebookCellMetadata]) + ) { + filteredMetadata[key] = this.metadata[key as keyof NotebookCellMetadata]; + } + } + + return filteredMetadata; + } + getTextLength(): number { return this.textBuffer.getLength(); } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index a45d181df50..c1a7165beb8 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -8,20 +8,12 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, ICellInsertEdit, NotebookCellsChangedEvent, CellKind, IProcessedOutput, notebookDocumentMetadataDefaults, diff, ICellDeleteEdit, NotebookCellsChangeType, ICellDto2, IMainCellDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, NotebookCellsChangedEvent, CellKind, IProcessedOutput, notebookDocumentMetadataDefaults, diff, NotebookCellsChangeType, ICellDto2, IMainCellDto, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextSnapshot } from 'vs/editor/common/model'; import { IUndoRedoService, UndoRedoElementType, IUndoRedoElement, IResourceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; -import { InsertCellEdit, DeleteCellEdit, MoveCellEdit, SpliceCellsEdit } from 'vs/workbench/contrib/notebook/common/model/cellEdit'; +import { InsertCellEdit, DeleteCellEdit, MoveCellEdit, SpliceCellsEdit, CellMetadataEdit } from 'vs/workbench/contrib/notebook/common/model/cellEdit'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -function compareRangesUsingEnds(a: [number, number], b: [number, number]): number { - if (a[1] === b[1]) { - return a[1] - b[1]; - - } - return a[1] - b[1]; -} - export class NotebookTextModelSnapshot implements ITextSnapshot { // private readonly _pieces: Ce[] = []; private _index: number = -1; @@ -136,7 +128,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel cells: NotebookCellTextModel[]; languages: string[] = []; metadata: NotebookDocumentMetadata = notebookDocumentMetadataDefaults; - renderers = new Set(); + transientOptions: TransientOptions = { transientMetadata: {}, transientOutputs: false }; private _isUntitled: boolean | undefined = undefined; private _versionId = 0; @@ -187,7 +179,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } createCellTextModel( - source: string | string[], + source: string, language: string, cellKind: CellKind, outputs: IProcessedOutput[], @@ -195,7 +187,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel ) { const cellHandle = this._cellhandlePool++; const cellUri = CellUri.generate(this.uri, cellHandle); - return new NotebookCellTextModel(cellUri, cellHandle, source, language, cellKind, outputs || [], metadata, this._modelService); + return new NotebookCellTextModel(cellUri, cellHandle, source, language, cellKind, outputs || [], metadata || {}, this.transientOptions, this._modelService); } initialize(cells: ICellDto2[]) { @@ -205,7 +197,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const mainCells = cells.map(cell => { const cellHandle = this._cellhandlePool++; const cellUri = CellUri.generate(this.uri, cellHandle); - return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata, this._modelService); + return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata, this.transientOptions, this._modelService); }); this._isUntitled = false; @@ -214,6 +206,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._mapping.set(mainCells[i].handle, mainCells[i]); const dirtyStateListener = mainCells[i].onDidChangeContent(() => { this.setDirty(true); + this._increaseVersionId(); this._onDidChangeContent.fire(); }); @@ -228,7 +221,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._operationManager.pushStackElement(label); } - $applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[], synchronous: boolean): boolean { + applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[], synchronous: boolean): boolean { if (modelVersionId !== this._versionId) { return false; } @@ -236,49 +229,30 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const oldViewCells = this.cells.slice(0); const oldMap = new Map(this._mapping); - let operations: ({ sortIndex: number; start: number; end: number; } & ICellEditOperation)[] = []; - for (let i = 0; i < rawEdits.length; i++) { - if (rawEdits[i].editType === CellEditType.Insert) { - const edit = rawEdits[i] as ICellInsertEdit; - operations.push({ - sortIndex: i, - start: edit.index, - end: edit.index, - ...edit - }); - } else { - const edit = rawEdits[i] as ICellDeleteEdit; - operations.push({ - sortIndex: i, - start: edit.index, - end: edit.index + edit.count, - ...edit - }); - } - } - - // const edits - operations = operations.sort((a, b) => { - const r = compareRangesUsingEnds([a.start, a.end], [b.start, b.end]); - if (r === 0) { - return b.sortIndex - a.sortIndex; - } - return -r; + const edits = rawEdits.map((edit, index) => { + return { + edit, + end: edit.editType === CellEditType.Replace ? edit.index + edit.count : edit.index, + originalIndex: index, + }; + }).sort((a, b) => { + return b.end - a.end || b.originalIndex - a.originalIndex; }); - for (let i = 0; i < operations.length; i++) { - switch (operations[i].editType) { - case CellEditType.Insert: - const insertEdit = operations[i] as ICellInsertEdit; - const mainCells = insertEdit.cells.map(cell => { - const cellHandle = this._cellhandlePool++; - const cellUri = CellUri.generate(this.uri, cellHandle); - return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata, this._modelService); - }); - this.insertNewCell(insertEdit.index, mainCells, false); + for (const { edit } of edits) { + switch (edit.editType) { + case CellEditType.Replace: + this._replaceCells(edit.index, edit.count, edit.cells); break; - case CellEditType.Delete: - this.removeCell(operations[i].index, operations[i].end - operations[i].start, false); + case CellEditType.Output: + //TODO@joh,@rebornix no event, no undo stop (?) + this.assertIndex(edit.index); + const cell = this.cells[edit.index]; + this.spliceNotebookCellOutputs(cell.handle, [[0, cell.outputs.length, edit.outputs]]); + break; + case CellEditType.Metadata: + this.assertIndex(edit.index); + this.deltaCellMetadata(this.cells[edit.index].handle, edit.metadata); break; } } @@ -320,7 +294,48 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return true; } - $handleEdit(label: string | undefined, undo: () => void, redo: () => void): void { + private _replaceCells(index: number, count: number, cellDtos: ICellDto2[]): void { + + if (count === 0 && cellDtos.length === 0) { + return; + } + + this._isUntitled = false; //TODO@rebornix fishy? + + // prepare remove + for (let i = index; i < index + count; i++) { + const cell = this.cells[i]; + this._cellListeners.get(cell.handle)?.dispose(); + this._cellListeners.delete(cell.handle); + } + + // prepare add + const cells = cellDtos.map(cellDto => { + const cellHandle = this._cellhandlePool++; + const cellUri = CellUri.generate(this.uri, cellHandle); + const cell = new NotebookCellTextModel( + cellUri, cellHandle, + cellDto.source, cellDto.language, cellDto.cellKind, cellDto.outputs || [], cellDto.metadata, this.transientOptions, + this._modelService + ); + const dirtyStateListener = cell.onDidChangeContent(() => { + this.setDirty(true); + this._increaseVersionId(); + this._onDidChangeContent.fire(); + }); + this._cellListeners.set(cell.handle, dirtyStateListener); + this._mapping.set(cell.handle, cell); + return cell; + }); + + // make change + this.cells.splice(index, count, ...cells); + this.setDirty(true); + this._increaseVersionId(); + this._onDidChangeContent.fire(); + } + + handleEdit(label: string | undefined, undo: () => void, redo: () => void): void { this._operationManager.pushEditOperation({ type: UndoRedoElementType.Resource, resource: this.uri, @@ -361,20 +376,6 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._onDidChangeMetadata.fire(this.metadata); } - updateNotebookCellMetadata(handle: number, metadata: NotebookCellMetadata) { - const cell = this.cells.find(cell => cell.handle === handle); - - if (cell) { - cell.metadata = metadata; - } - } - - updateRenderers(renderers: string[]) { - renderers.forEach(render => { - this.renderers.add(render); - }); - } - insertTemplateCell(cell: NotebookCellTextModel) { if (this.cells.length > 0 || this._isUntitled !== undefined) { return; @@ -387,6 +388,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const dirtyStateListener = cell.onDidChangeContent(() => { this._isUntitled = false; this.setDirty(true); + this._increaseVersionId(); this._onDidChangeContent.fire(); }); @@ -423,6 +425,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._mapping.set(cells[i].handle, cells[i]); const dirtyStateListener = cells[i].onDidChangeContent(() => { this.setDirty(true); + this._increaseVersionId(); this._onDidChangeContent.fire(); }); @@ -477,11 +480,11 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } } - moveCellToIdx(index: number, newIdx: number, emitToExtHost: boolean = true) { + moveCellToIdx(index: number, length: number, newIdx: number, emitToExtHost: boolean = true) { this.assertIndex(index); this.assertIndex(newIdx); - const cells = this.cells.splice(index, 1); + const cells = this.cells.splice(index, length); this.cells.splice(newIdx, 0, ...cells); this.setDirty(true); this._onDidChangeContent.fire(); @@ -500,9 +503,26 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } // TODO@rebornix should this trigger content change event? - $spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]): void { + spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]): void { const cell = this._mapping.get(cellHandle); - cell?.spliceNotebookCellOutputs(splices); + if (cell) { + + cell.spliceNotebookCellOutputs(splices); + + if (!this.transientOptions.transientOutputs) { + this._increaseVersionId(); + this.setDirty(true); + this._onDidChangeContent.fire(); + } + + this._onDidModelChangeProxy.fire({ + kind: NotebookCellsChangeType.Output, + versionId: this.versionId, + index: this.cells.indexOf(cell), + outputs: cell.outputs ?? [] + }); + } + } clearCellOutput(handle: number) { @@ -527,16 +547,62 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } } - changeCellMetadata(handle: number, newMetadata: NotebookCellMetadata) { + private _isCellMetadataChanged(a: NotebookCellMetadata, b: NotebookCellMetadata) { + const keys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})]); + for (let key of keys) { + if ( + (a[key as keyof NotebookCellMetadata] !== b[key as keyof NotebookCellMetadata]) + && + !(this.transientOptions.transientMetadata[key as keyof NotebookCellMetadata]) + ) { + return true; + } + } + + return false; + } + + changeCellMetadata(handle: number, metadata: NotebookCellMetadata, pushUndoStop: boolean) { + const cell = this.cells.find(cell => cell.handle === handle); + + if (!cell) { + return; + } + + const triggerDirtyChange = this._isCellMetadataChanged(cell.metadata, metadata); + + if (triggerDirtyChange) { + if (pushUndoStop) { + const index = this.cells.indexOf(cell); + this._operationManager.pushEditOperation(new CellMetadataEdit(this.uri, index, Object.freeze(cell.metadata), Object.freeze(metadata), { + updateCellMetadata: (index, newMetadata) => { + const cell = this.cells[index]; + if (!cell) { + return; + } + this.changeCellMetadata(cell.handle, newMetadata, false); + }, + emitSelections: this._emitSelectionsDelegate.bind(this) + })); + } + cell.metadata = metadata; + this.setDirty(true); + this._onDidChangeContent.fire(); + } else { + cell.metadata = metadata; + } + + this._increaseVersionId(); + this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ChangeMetadata, versionId: this._versionId, index: this.cells.indexOf(cell), metadata: cell.metadata }); + } + + deltaCellMetadata(handle: number, newMetadata: NotebookCellMetadata) { const cell = this._mapping.get(handle); if (cell) { - cell.metadata = { + this.changeCellMetadata(handle, { ...cell.metadata, ...newMetadata - }; - - this._increaseVersionId(); - this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ChangeMetadata, versionId: this._versionId, index: this.cells.indexOf(cell), metadata: cell.metadata }); + }, true); } } @@ -566,7 +632,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._emitSelections.fire(selections); } - createCell2(index: number, source: string | string[], language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, synchronous: boolean, pushUndoStop: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined) { + createCell2(index: number, source: string, language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, synchronous: boolean, pushUndoStop: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined) { const cell = this.createCellTextModel(source, language, type, [], metadata); if (pushUndoStop) { @@ -618,22 +684,22 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } } - moveCellToIdx2(index: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined): boolean { - const cell = this.cells[index]; + moveCellToIdx2(index: number, length: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined): boolean { + const cells = this.cells.slice(index, index + length); if (pushedToUndoStack) { - this._operationManager.pushEditOperation(new MoveCellEdit(this.uri, index, newIdx, { - moveCell: (fromIndex: number, toIndex: number, beforeSelections: number[] | undefined, endSelections: number[] | undefined) => { - this.moveCellToIdx2(fromIndex, toIndex, true, false, beforeSelections, endSelections); + this._operationManager.pushEditOperation(new MoveCellEdit(this.uri, index, length, newIdx, { + moveCell: (fromIndex: number, length: number, toIndex: number, beforeSelections: number[] | undefined, endSelections: number[] | undefined) => { + this.moveCellToIdx2(fromIndex, length, toIndex, true, false, beforeSelections, endSelections); }, emitSelections: this._emitSelectionsDelegate.bind(this) }, beforeSelections, endSelections)); } - this.moveCellToIdx(index, newIdx); + this.moveCellToIdx(index, length, newIdx); // todo, we can't emit this change as it will create a new view model and that will hold // a new reference to the document, thus - this._onDidChangeCells.fire({ synchronous: synchronous, splices: [[index, 1, []]] }); - this._onDidChangeCells.fire({ synchronous: synchronous, splices: [[newIdx, 0, [cell]]] }); + this._onDidChangeCells.fire({ synchronous: synchronous, splices: [[index, length, []]] }); + this._onDidChangeCells.fire({ synchronous: synchronous, splices: [[newIdx, 0, cells]] }); if (endSelections) { this._emitSelections.fire(endSelections); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookCellStatusBarService.ts b/src/vs/workbench/contrib/notebook/common/notebookCellStatusBarService.ts new file mode 100644 index 00000000000..e99fa061a11 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/notebookCellStatusBarService.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { INotebookCellStatusBarEntry } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +export const INotebookCellStatusBarService = createDecorator('notebookCellStatusBarService'); + +export interface INotebookCellStatusBarService { + readonly _serviceBrand: undefined; + + onDidChangeEntriesForCell: Event; + + addEntry(entry: INotebookCellStatusBarEntry): IDisposable; + getEntries(cell: URI): INotebookCellStatusBarEntry[]; +} diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 2bddcefc19c..64127dec611 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -3,21 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDiffResult, ISequence } from 'vs/base/common/diff/diff'; import { Event } from 'vs/base/common/event'; import * as glob from 'vs/base/common/glob'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import * as UUID from 'vs/base/common/uuid'; +import { Schemas } from 'vs/base/common/network'; +import { basename } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; import { ISplice } from 'vs/base/common/sequence'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { Command } from 'vs/editor/common/modes'; +import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IEditorModel } from 'vs/platform/editor/common/editor'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { Schemas } from 'vs/base/common/network'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IRevertOptions } from 'vs/workbench/common/editor'; -import { basename } from 'vs/base/common/path'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { IDisposable } from 'vs/base/common/lifecycle'; export enum CellKind { Markdown = 1, @@ -102,6 +106,13 @@ export interface NotebookCellMetadata { custom?: { [key: string]: unknown }; } +export type TransientMetadata = { [K in keyof NotebookCellMetadata]?: boolean }; + +export interface TransientOptions { + transientOutputs: boolean; + transientMetadata: TransientMetadata; +} + export interface INotebookDisplayOrder { defaultOrder: string[]; userOrder?: string[]; @@ -113,34 +124,13 @@ export interface INotebookMimeTypeSelector { export interface INotebookRendererInfo { id: string; - extensionId: ExtensionIdentifier; - extensionLocation: URI, - preloads: URI[], - render(uri: URI, request: IOutputRenderRequest): Promise | undefined>; - render2(uri: URI, request: IOutputRenderRequest): Promise | undefined>; -} - -export interface INotebookKernelInfo { - id: string; - label: string, - selectors: (string | glob.IRelativePattern)[], - extension: ExtensionIdentifier; - extensionLocation: URI, - preloads: URI[]; - providerHandle?: number; - executeNotebook(viewType: string, uri: URI, handle: number | undefined): Promise; - -} - -export interface INotebookKernelInfoDto { - id: string; - label: string, + displayName: string; + entrypoint: URI; + preloads: ReadonlyArray; extensionLocation: URI; - preloads?: UriComponents[]; -} + extensionId: ExtensionIdentifier; -export interface INotebookSelectors { - readonly filenamePattern?: string; + matches(mimeType: string): boolean; } export interface IStreamOutput { @@ -153,15 +143,15 @@ export interface IErrorOutput { /** * Exception Name */ - ename?: string; + ename: string; /** * Exception Value */ - evalue?: string; + evalue: string; /** * Exception call stacks */ - traceback?: string[]; + traceback: string[]; } export interface NotebookCellOutputMetadata { @@ -189,9 +179,7 @@ export enum MimeTypeRendererResolver { export interface IOrderedMimeType { mimeType: string; - isResolved: boolean; - rendererId?: string; - output?: string; + rendererId: string; } export interface ITransformedDisplayOutputDto { @@ -204,6 +192,10 @@ export interface ITransformedDisplayOutputDto { pickedMimeTypeIndex?: number; } +export function isTransformedDisplayOutput(thing: unknown): thing is ITransformedDisplayOutputDto { + return (thing as ITransformedDisplayOutputDto).outputKind === CellOutputKind.Rich && !!(thing as ITransformedDisplayOutputDto).outputId; +} + export interface IGenericOutput { outputKind: CellOutputKind; pickedMimeType?: string; @@ -211,6 +203,11 @@ export interface IGenericOutput { transformedOutput?: { [key: string]: IDisplayOutput }; } + +export const addIdToOutput = (output: IRawOutput, id = UUID.generateUuid()): IProcessedOutput => output.outputKind === CellOutputKind.Rich + ? ({ ...output, outputId: id }) : output; + + export type IProcessedOutput = ITransformedDisplayOutputDto | IStreamOutput | IErrorOutput; export type IRawOutput = IDisplayOutput | IStreamOutput | IErrorOutput; @@ -279,17 +276,41 @@ export interface INotebookTextModel { readonly versionId: number; languages: string[]; cells: ICell[]; - renderers: Set; onDidChangeCells?: Event<{ synchronous: boolean, splices: NotebookCellTextModelSplice[] }>; onDidChangeContent: Event; onWillDispose(listener: () => void): IDisposable; } -export interface IRenderOutput { - shadowContent?: string; +export const enum RenderOutputType { + None, + Html, + Extension +} + +export interface IRenderNoOutput { + type: RenderOutputType.None; hasDynamicHeight: boolean; } +export interface IRenderPlainHtmlOutput { + type: RenderOutputType.Html; + source: ITransformedDisplayOutputDto; + htmlContent: string; + hasDynamicHeight: boolean; +} + +export interface IRenderOutputViaExtension { + type: RenderOutputType.Extension; + source: ITransformedDisplayOutputDto; + mimeType: string; + renderer: INotebookRendererInfo; +} + +export type IInsetRenderOutput = IRenderPlainHtmlOutput | IRenderOutputViaExtension; +export type IRenderOutput = IRenderNoOutput | IInsetRenderOutput; + +export const outputHasDynamicHeight = (o: IRenderOutput) => o.type !== RenderOutputType.Extension && o.hasDynamicHeight; + export type NotebookCellTextModelSplice = [ number /* start */, number, @@ -326,7 +347,8 @@ export enum NotebookCellsChangeType { CellsClearOutput = 4, ChangeLanguage = 5, Initialize = 6, - ChangeMetadata = 7 + ChangeMetadata = 7, + Output = 8, } export interface NotebookCellsInitializeEvent { @@ -348,6 +370,13 @@ export interface NotebookCellsModelMoveEvent { readonly versionId: number; } +export interface NotebookOutputChangedEvent { + readonly kind: NotebookCellsChangeType.Output; + readonly index: number; + readonly versionId: number; + readonly outputs: IProcessedOutput[]; +} + export interface NotebookCellClearOutputEvent { readonly kind: NotebookCellsChangeType.CellClearOutput; readonly index: number; @@ -370,41 +399,49 @@ export interface NotebookCellsChangeMetadataEvent { readonly kind: NotebookCellsChangeType.ChangeMetadata; readonly versionId: number; readonly index: number; - readonly metadata: NotebookCellMetadata; + readonly metadata: NotebookCellMetadata | undefined; } -export type NotebookCellsChangedEvent = NotebookCellsInitializeEvent | NotebookCellsModelChangedEvent | NotebookCellsModelMoveEvent | NotebookCellClearOutputEvent | NotebookCellsClearOutputEvent | NotebookCellsChangeLanguageEvent | NotebookCellsChangeMetadataEvent; -export enum CellEditType { - Insert = 1, - Delete = 2 +export type NotebookCellsChangedEvent = NotebookCellsInitializeEvent | NotebookCellsModelChangedEvent | NotebookCellsModelMoveEvent | NotebookOutputChangedEvent | NotebookCellClearOutputEvent | NotebookCellsClearOutputEvent | NotebookCellsChangeLanguageEvent | NotebookCellsChangeMetadataEvent; + +export const enum CellEditType { + Replace = 1, + Output = 2, + Metadata = 3, } export interface ICellDto2 { - source: string | string[]; + source: string; language: string; cellKind: CellKind; outputs: IProcessedOutput[]; metadata?: NotebookCellMetadata; } -export interface ICellInsertEdit { - editType: CellEditType.Insert; +export interface ICellReplaceEdit { + editType: CellEditType.Replace; index: number; + count: number; cells: ICellDto2[]; } -export interface ICellDeleteEdit { - editType: CellEditType.Delete; +export interface ICellOutputEdit { + editType: CellEditType.Output; index: number; - count: number; + outputs: IProcessedOutput[]; } -export type ICellEditOperation = ICellInsertEdit | ICellDeleteEdit; +export interface ICellMetadataEdit { + editType: CellEditType.Metadata; + index: number; + metadata: NotebookCellMetadata; +} + +export type ICellEditOperation = ICellReplaceEdit | ICellOutputEdit | ICellMetadataEdit; export interface INotebookEditData { documentVersionId: number; edits: ICellEditOperation[]; - renderers: number[]; } export interface NotebookDataDto { @@ -425,7 +462,8 @@ export function getCellUndoRedoComparisonKey(uri: URI) { export namespace CellUri { - export const scheme = 'vscode-notebook-cell'; + export const scheme = Schemas.vscodeNotebookCell; + const _regex = /^\d{7,}/; export function generate(notebook: URI, handle: number): URI { @@ -435,6 +473,14 @@ export namespace CellUri { }); } + export function generateCellMetadataUri(notebook: URI, handle: number): URI { + return notebook.with({ + scheme: Schemas.vscode, + authority: 'vscode-notebook-cell-metadata', + fragment: `${handle.toString().padStart(7, '0')}${notebook.scheme !== Schemas.file ? notebook.scheme : ''}` + }); + } + export function parse(cell: URI): { notebook: URI, handle: number } | undefined { if (cell.scheme !== scheme) { return undefined; @@ -526,7 +572,7 @@ interface IMutableSplice extends ISplice { deleteCount: number; } -export function diff(before: T[], after: T[], contains: (a: T) => boolean): ISplice[] { +export function diff(before: T[], after: T[], contains: (a: T) => boolean, equal: (a: T, b: T) => boolean = (a: T, b: T) => a === b): ISplice[] { const result: IMutableSplice[] = []; function pushSplice(start: number, deleteCount: number, toInsert: T[]): void { @@ -561,7 +607,7 @@ export function diff(before: T[], after: T[], contains: (a: T) => boolean): I const beforeElement = before[beforeIdx]; const afterElement = after[afterIdx]; - if (beforeElement === afterElement) { + if (equal(beforeElement, afterElement)) { // equal beforeIdx += 1; afterIdx += 1; @@ -601,6 +647,12 @@ export interface INotebookEditorModel extends IEditorModel { revert(options?: IRevertOptions | undefined): Promise; } +export interface INotebookDiffEditorModel extends IEditorModel { + original: INotebookEditorModel; + modified: INotebookEditorModel; + resolveOriginalFromDisk(): Promise; +} + export interface INotebookTextModelBackup { metadata: NotebookDocumentMetadata; languages: string[]; @@ -614,10 +666,27 @@ export interface NotebookDocumentBackupData { readonly mtime?: number; } +/** + * [start, end] + */ +export interface ICellRange { + /** + * zero based index + */ + start: number; + + /** + * zero based index + */ + end: number; +} + export interface IEditor extends editorCommon.ICompositeCodeEditor { readonly onDidChangeModel: Event; readonly onDidFocusEditorWidget: Event; + readonly onDidChangeVisibleRanges: Event; isNotebookEditor: boolean; + visibleRanges: ICellRange[]; uri?: URI; textModel?: NotebookTextModel; getId(): string; @@ -637,22 +706,44 @@ export interface INotebookSearchOptions { wordSeparators?: string; } +export interface INotebookExclusiveDocumentFilter { + include?: string | glob.IRelativePattern; + exclude?: string | glob.IRelativePattern; +} + export interface INotebookDocumentFilter { - viewType?: string; - filenamePattern?: string | glob.IRelativePattern; - excludeFileNamePattern?: string | glob.IRelativePattern; + viewType?: string | string[]; + filenamePattern?: string | glob.IRelativePattern | INotebookExclusiveDocumentFilter; } //TODO@rebornix test + +function isDocumentExcludePattern(filenamePattern: string | glob.IRelativePattern | INotebookExclusiveDocumentFilter): filenamePattern is { include: string | glob.IRelativePattern; exclude: string | glob.IRelativePattern; } { + const arg = filenamePattern as INotebookExclusiveDocumentFilter; + + if ((typeof arg.include === 'string' || glob.isRelativePattern(arg.include)) + && (typeof arg.exclude === 'string' || glob.isRelativePattern(arg.exclude))) { + return true; + } + + return false; +} export function notebookDocumentFilterMatch(filter: INotebookDocumentFilter, viewType: string, resource: URI): boolean { + if (Array.isArray(filter.viewType) && filter.viewType.indexOf(viewType) >= 0) { + return true; + } + if (filter.viewType === viewType) { return true; } if (filter.filenamePattern) { - if (glob.match(filter.filenamePattern, basename(resource.fsPath).toLowerCase())) { - if (filter.excludeFileNamePattern) { - if (glob.match(filter.excludeFileNamePattern, basename(resource.fsPath).toLowerCase())) { + let filenamePattern = isDocumentExcludePattern(filter.filenamePattern) ? filter.filenamePattern.include : (filter.filenamePattern as string | glob.IRelativePattern); + let excludeFilenamePattern = isDocumentExcludePattern(filter.filenamePattern) ? filter.filenamePattern.exclude : undefined; + + if (glob.match(filenamePattern, basename(resource.fsPath).toLowerCase())) { + if (excludeFilenamePattern) { + if (glob.match(excludeFilenamePattern, basename(resource.fsPath).toLowerCase())) { // should exclude return false; @@ -671,6 +762,7 @@ export interface INotebookKernelInfoDto2 { extensionLocation: URI; providerHandle?: number; description?: string; + detail?: string; isPreferred?: boolean; preloads?: UriComponents[]; } @@ -685,9 +777,50 @@ export interface INotebookKernelProvider { providerExtensionId: string; providerDescription?: string; selector: INotebookDocumentFilter; - onDidChangeKernels: Event; + onDidChangeKernels: Event; provideKernels(uri: URI, token: CancellationToken): Promise; resolveKernel(editorId: string, uri: UriComponents, kernelId: string, token: CancellationToken): Promise; executeNotebook(uri: URI, kernelId: string, handle: number | undefined): Promise; cancelNotebook(uri: URI, kernelId: string, handle: number | undefined): Promise; } + +export class CellSequence implements ISequence { + + constructor(readonly textModel: NotebookTextModel) { + } + + getElements(): string[] | number[] | Int32Array { + const hashValue = new Int32Array(this.textModel.cells.length); + for (let i = 0; i < this.textModel.cells.length; i++) { + hashValue[i] = this.textModel.cells[i].getHashValue(); + } + + return hashValue; + } +} + +export interface INotebookDiffResult { + cellsDiff: IDiffResult, + linesDiff?: { originalCellhandle: number, modifiedCellhandle: number, lineChanges: editorCommon.ILineChange[] }[]; +} + +export interface INotebookCellStatusBarEntry { + readonly cellResource: URI; + readonly alignment: CellStatusbarAlignment; + readonly priority?: number; + readonly text: string; + readonly tooltip: string | undefined; + readonly command: string | Command | undefined; + readonly accessibilityInformation?: IAccessibilityInformation; + readonly visible: boolean; +} + +export const DisplayOrderKey = 'notebook.displayOrder'; +export const CellToolbarLocKey = 'notebook.cellToolbarLocation'; +export const ShowCellStatusbarKey = 'notebook.showCellStatusbar'; +export const NotebookTextDiffEditorPreview = 'notebook.diff.enablePreview'; + +export const enum CellStatusbarAlignment { + LEFT, + RIGHT +} diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index a0e064a2aec..2c750fafbce 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -11,20 +11,13 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { URI } from 'vs/base/common/uri'; import { IWorkingCopyService, IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { basename } from 'vs/base/common/resources'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { Schemas } from 'vs/base/common/network'; import { IFileStatWithMetadata, IFileService } from 'vs/platform/files/common/files'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { ILabelService } from 'vs/platform/label/common/label'; -export interface INotebookEditorModelManager { - models: NotebookEditorModel[]; - - resolve(resource: URI, viewType: string, editorId?: string): Promise; - - get(resource: URI): NotebookEditorModel | undefined; -} export interface INotebookLoadOptions { /** @@ -36,7 +29,7 @@ export interface INotebookLoadOptions { } -export class NotebookEditorModel extends EditorModel implements IWorkingCopy, INotebookEditorModel { +export class NotebookEditorModel extends EditorModel implements INotebookEditorModel { protected readonly _onDidChangeDirty = this._register(new Emitter()); readonly onDidChangeDirty = this._onDidChangeDirty.event; private readonly _onDidChangeContent = this._register(new Emitter()); @@ -48,31 +41,29 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN return this._notebook; } - private _name!: string; - - get name() { - return this._name; - } - - private _workingCopyResource: URI; + private readonly _name: string; + private readonly _workingCopyResource: URI; constructor( - public readonly resource: URI, - public readonly viewType: string, + readonly resource: URI, + readonly viewType: string, @INotebookService private readonly _notebookService: INotebookService, @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, @IBackupFileService private readonly _backupFileService: IBackupFileService, @IFileService private readonly _fileService: IFileService, - @INotificationService private readonly _notificationService: INotificationService + @INotificationService private readonly _notificationService: INotificationService, + @ILabelService labelService: ILabelService, ) { super(); + this._name = labelService.getUriBasenameLabel(resource); + const input = this; this._workingCopyResource = resource.with({ scheme: Schemas.vscodeNotebook }); const workingCopyAdapter = new class implements IWorkingCopy { readonly resource = input._workingCopyResource; - get name() { return input.name; } - readonly capabilities = input.isUntitled() ? WorkingCopyCapabilities.Untitled : input.capabilities; + get name() { return input._name; } + readonly capabilities = input.isUntitled() ? WorkingCopyCapabilities.Untitled : WorkingCopyCapabilities.None; readonly onDidChangeDirty = input.onDidChangeDirty; readonly onDidChangeContent = input.onDidChangeContent; isDirty(): boolean { return input.isDirty(); } @@ -84,8 +75,6 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN this._register(this._workingCopyService.registerWorkingCopy(workingCopyAdapter)); } - capabilities = 0; - async backup(): Promise> { if (this._notebook.supportBackup) { const tokenSource = new CancellationTokenSource(); @@ -128,7 +117,7 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN async load(options?: INotebookLoadOptions): Promise { if (options?.forceReadFromDisk) { - return this.loadFromProvider(true, undefined, undefined); + return this._loadFromProvider(true, undefined, undefined); } if (this.isResolved()) { @@ -141,19 +130,17 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN return this; // Make sure meanwhile someone else did not succeed in loading } - return this.loadFromProvider(false, options?.editorId, backup?.meta?.backupId); + return this._loadFromProvider(false, options?.editorId, backup?.meta?.backupId); } - private async loadFromProvider(forceReloadFromDisk: boolean, editorId: string | undefined, backupId: string | undefined) { - const notebook = await this._notebookService.resolveNotebook(this.viewType!, this.resource, forceReloadFromDisk, editorId, backupId); - this._notebook = notebook!; + private async _loadFromProvider(forceReloadFromDisk: boolean, editorId: string | undefined, backupId: string | undefined) { + this._notebook = await this._notebookService.resolveNotebook(this.viewType!, this.resource, forceReloadFromDisk, editorId, backupId); + const newStats = await this._resolveStats(this.resource); this._lastResolvedFileStat = newStats; this._register(this._notebook); - this._name = basename(this._notebook!.uri); - this._register(this._notebook.onDidChangeContent(() => { this._onDidChangeContent.fire(); })); @@ -261,10 +248,5 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN } catch (e) { return undefined; } - - } - - dispose() { - super.dispose(); } } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts index cedfaeef088..2206d05e61e 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts @@ -7,15 +7,16 @@ import { createDecorator, IInstantiationService } from 'vs/platform/instantiatio import { URI } from 'vs/base/common/uri'; import { INotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; -import { IReference, ReferenceCollection } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, IReference, ReferenceCollection } from 'vs/base/common/lifecycle'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { ILogService } from 'vs/platform/log/common/log'; +import { Event } from 'vs/base/common/event'; export const INotebookEditorModelResolverService = createDecorator('INotebookModelResolverService'); export interface INotebookEditorModelResolverService { readonly _serviceBrand: undefined; - resolve(resource: URI, viewType: string, editorId?: string): Promise>; + resolve(resource: URI, viewType?: string, editorId?: string): Promise>; } @@ -30,9 +31,16 @@ export class NotebookModelReferenceCollection extends ReferenceCollection { - const [viewType, editorId] = args as [string, string | undefined]; - const resource = URI.parse(key); + + let [viewType, editorId] = args as [string | undefined, string | undefined]; + if (!viewType) { + viewType = this._notebookService.getContributedNotebookProviders(resource)[0]?.id; + } + if (!viewType) { + throw new Error('Missing viewType'); + } + const model = this._instantiationService.createInstance(NotebookEditorModel, resource, viewType); const promise = model.load({ editorId }); return promise; @@ -60,12 +68,30 @@ export class NotebookModelResolverService implements INotebookEditorModelResolve this._data = instantiationService.createInstance(NotebookModelReferenceCollection); } - async resolve(resource: URI, viewType: string, editorId?: string | undefined): Promise> { + async resolve(resource: URI, viewType?: string, editorId?: string | undefined): Promise> { const reference = this._data.acquire(resource.toString(), viewType, editorId); const model = await reference.object; + NotebookModelResolverService._autoReferenceDirtyModel(model, () => this._data.acquire(resource.toString(), viewType, editorId)); return { object: model, dispose() { reference.dispose(); } }; } + + private static _autoReferenceDirtyModel(model: INotebookEditorModel, ref: () => IDisposable) { + + const references = new DisposableStore(); + const listener = model.notebook.onDidChangeDirty(() => { + if (model.notebook.isDirty) { + references.add(ref()); + } else { + references.clear(); + } + }); + + Event.once(model.notebook.onWillDispose)(() => { + listener.dispose(); + references.dispose(); + }); + } } diff --git a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts index b22c64145eb..4ac836f1f9d 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts @@ -4,31 +4,42 @@ *--------------------------------------------------------------------------------------------*/ import * as glob from 'vs/base/common/glob'; +import { joinPath } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -export class NotebookOutputRendererInfo { +export class NotebookOutputRendererInfo implements INotebookRendererInfo { readonly id: string; + readonly entrypoint: URI; readonly displayName: string; - readonly mimeTypes: readonly string[]; - readonly mimeTypeGlobs: glob.ParsedPattern[]; + readonly extensionLocation: URI; + readonly extensionId: ExtensionIdentifier; + // todo: re-add preloads in pure renderer API + readonly preloads: ReadonlyArray = []; + + private readonly mimeTypes: readonly string[]; + private readonly mimeTypeGlobs: glob.ParsedPattern[]; constructor(descriptor: { readonly id: string; readonly displayName: string; + readonly entrypoint: string; readonly mimeTypes: readonly string[]; + readonly extension: IExtensionDescription; }) { this.id = descriptor.id; + this.extensionId = descriptor.extension.identifier; + this.extensionLocation = descriptor.extension.extensionLocation; + this.entrypoint = joinPath(this.extensionLocation, descriptor.entrypoint); this.displayName = descriptor.displayName; this.mimeTypes = descriptor.mimeTypes; this.mimeTypeGlobs = this.mimeTypes.map(pattern => glob.parse(pattern)); } matches(mimeType: string) { - const matched = this.mimeTypeGlobs.find(pattern => pattern(mimeType)); - if (matched) { - return true; - } - - return this.mimeTypes.find(pattern => pattern === mimeType); + return this.mimeTypeGlobs.some(pattern => pattern(mimeType)) + || this.mimeTypes.some(pattern => pattern === mimeType); } } diff --git a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts index 2ffcad9a909..3649b54141c 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts @@ -6,7 +6,7 @@ import * as glob from 'vs/base/common/glob'; import { URI } from 'vs/base/common/uri'; import { basename } from 'vs/base/common/path'; -import { INotebookKernelInfoDto, NotebookEditorPriority } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorPriority } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export interface NotebookSelector { readonly filenamePattern?: string; @@ -22,7 +22,6 @@ export interface NotebookEditorDescriptor { readonly providerDescription?: string; readonly providerDisplayName: string; readonly providerExtensionLocation: URI; - kernel?: INotebookKernelInfoDto; } export class NotebookProviderInfo implements NotebookEditorDescriptor { @@ -36,7 +35,6 @@ export class NotebookProviderInfo implements NotebookEditorDescriptor { readonly providerDescription?: string; readonly providerDisplayName: string; readonly providerExtensionLocation: URI; - kernel?: INotebookKernelInfoDto; constructor(descriptor: NotebookEditorDescriptor) { this.id = descriptor.id; diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 9c6d199d7ed..40e62b67084 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -9,28 +9,25 @@ import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/noteb import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; import { Event } from 'vs/base/common/event'; import { - INotebookTextModel, INotebookRendererInfo, INotebookKernelInfo, INotebookKernelInfoDto, - IEditor, ICellEditOperation, NotebookCellOutputsSplice, IOrderedMimeType, IProcessedOutput, INotebookKernelProvider, INotebookKernelInfo2 + INotebookTextModel, INotebookRendererInfo, + IEditor, ICellEditOperation, NotebookCellOutputsSplice, INotebookKernelProvider, INotebookKernelInfo2, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CancellationToken } from 'vs/base/common/cancellation'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; export const INotebookService = createDecorator('notebookService'); export interface IMainNotebookController { - kernel: INotebookKernelInfoDto | undefined; supportBackup: boolean; + options: { transientOutputs: boolean; transientMetadata: TransientMetadata; }; createNotebook(textModel: NotebookTextModel, editorId?: string, backupId?: string): Promise; reloadNotebook(mainthreadTextModel: NotebookTextModel): Promise; resolveNotebookEditor(viewType: string, uri: URI, editorId: string): Promise; - executeNotebookByAttachedKernel(viewType: string, uri: URI): Promise; - cancelNotebookByAttachedKernel(viewType: string, uri: URI): Promise; onDidReceiveMessage(editorId: string, rendererType: string | undefined, message: any): void; - executeNotebookCell(uri: URI, handle: number): Promise; - cancelNotebookCell(uri: URI, handle: number): Promise; removeNotebookDocument(uri: URI): Promise; save(uri: URI, token: CancellationToken): Promise; saveAs(uri: URI, target: URI, token: CancellationToken): Promise; @@ -47,29 +44,20 @@ export interface INotebookService { onNotebookDocumentRemove: Event; onNotebookDocumentAdd: Event; onNotebookDocumentSaved: Event; - onDidChangeKernels: Event; + onDidChangeKernels: Event; onDidChangeNotebookActiveKernel: Event<{ uri: URI, providerHandle: number | undefined, kernelId: string | undefined }>; registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: IMainNotebookController): void; unregisterNotebookProvider(viewType: string): void; - registerNotebookRenderer(id: string, renderer: INotebookRendererInfo): void; - unregisterNotebookRenderer(id: string): void; - transformEditsOutputs(textModel: NotebookTextModel, edits: ICellEditOperation[]): Promise; - transformSpliceOutputs(textModel: NotebookTextModel, splices: NotebookCellOutputsSplice[]): Promise; - transformSingleOutput(textModel: NotebookTextModel, output: IProcessedOutput, rendererId: string, mimeType: string): Promise; - registerNotebookKernel(kernel: INotebookKernelInfo): void; - unregisterNotebookKernel(id: string): void; + transformEditsOutputs(textModel: NotebookTextModel, edits: ICellEditOperation[]): void; + transformSpliceOutputs(textModel: NotebookTextModel, splices: NotebookCellOutputsSplice[]): void; registerNotebookKernelProvider(provider: INotebookKernelProvider): IDisposable; - getContributedNotebookKernels(viewType: string, resource: URI): readonly INotebookKernelInfo[]; getContributedNotebookKernels2(viewType: string, resource: URI, token: CancellationToken): Promise; + getContributedNotebookOutputRenderers(id: string): NotebookOutputRendererInfo | undefined; getRendererInfo(id: string): INotebookRendererInfo | undefined; - resolveNotebook(viewType: string, uri: URI, forceReload: boolean, editorId?: string, backupId?: string): Promise; + + resolveNotebook(viewType: string, uri: URI, forceReload: boolean, editorId?: string, backupId?: string): Promise; getNotebookTextModel(uri: URI): NotebookTextModel | undefined; - executeNotebook(viewType: string, uri: URI): Promise; - cancelNotebook(viewType: string, uri: URI): Promise; - executeNotebookCell(viewType: string, uri: URI, handle: number): Promise; - cancelNotebookCell(viewType: string, uri: URI, handle: number): Promise; - executeNotebook2(viewType: string, uri: URI, kernelId: string): Promise; - executeNotebookCell2(viewType: string, uri: URI, handle: number, kernelId: string): Promise; + getNotebookTextModels(): Iterable; getContributedNotebookProviders(resource: URI): readonly NotebookProviderInfo[]; getContributedNotebookProvider(viewType: string): NotebookProviderInfo | undefined; getNotebookProviderResourceRoots(): URI[]; diff --git a/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts b/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts new file mode 100644 index 00000000000..547f3678545 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts @@ -0,0 +1,221 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { ISequence, LcsDiff } from 'vs/base/common/diff/diff'; +import { hash } from 'vs/base/common/hash'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IRequestHandler } from 'vs/base/common/worker/simpleWorker'; +import * as model from 'vs/editor/common/model'; +import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; +import { CellKind, ICellDto2, IMainCellDto, INotebookDiffResult, IProcessedOutput, NotebookCellMetadata, NotebookDataDto, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { Range } from 'vs/editor/common/core/range'; +import { EditorWorkerHost } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl'; + +class MirrorCell { + private _textBuffer!: model.IReadonlyTextBuffer; + + get textBuffer() { + if (this._textBuffer) { + return this._textBuffer; + } + + const builder = new PieceTreeTextBufferBuilder(); + builder.acceptChunk(Array.isArray(this._source) ? this._source.join('\n') : this._source); + const bufferFactory = builder.finish(true); + this._textBuffer = bufferFactory.create(model.DefaultEndOfLine.LF); + + return this._textBuffer; + } + + private _primaryKey?: number | null = null; + primaryKey(): number | null { + if (this._primaryKey === undefined) { + this._primaryKey = hash(this.getValue()); + } + + return this._primaryKey; + } + + private _hash: number | null = null; + + constructor( + readonly handle: number, + private _source: string | string[], + readonly language: string, + readonly cellKind: CellKind, + readonly outputs: IProcessedOutput[], + readonly metadata?: NotebookCellMetadata + + ) { } + + getFullModelRange() { + const lineCount = this.textBuffer.getLineCount(); + return new Range(1, 1, lineCount, this.textBuffer.getLineLength(lineCount) + 1); + } + + getValue(): string { + const fullRange = this.getFullModelRange(); + const eol = this.textBuffer.getEOL(); + if (eol === '\n') { + return this.textBuffer.getValueInRange(fullRange, model.EndOfLinePreference.LF); + } else { + return this.textBuffer.getValueInRange(fullRange, model.EndOfLinePreference.CRLF); + } + } + + getComparisonValue(): number { + if (this._primaryKey !== null) { + return this._primaryKey!; + } + + this._hash = hash([hash(this.getValue()), this.metadata]); + return this._hash; + } + + getHashValue() { + if (this._hash !== null) { + return this._hash; + } + + this._hash = hash([hash(this.getValue()), this.language, this.metadata]); + return this._hash; + } +} + +class MirrorNotebookDocument { + constructor( + readonly uri: URI, + readonly cells: MirrorCell[], + readonly languages: string[], + readonly metadata: NotebookDocumentMetadata, + ) { + } +} + +export class CellSequence implements ISequence { + + constructor(readonly textModel: MirrorNotebookDocument) { + } + + getElements(): string[] | number[] | Int32Array { + const hashValue = new Int32Array(this.textModel.cells.length); + for (let i = 0; i < this.textModel.cells.length; i++) { + hashValue[i] = this.textModel.cells[i].getComparisonValue(); + } + + return hashValue; + } + + getCellHash(cell: ICellDto2) { + const source = Array.isArray(cell.source) ? cell.source.join('\n') : cell.source; + const hashVal = hash([hash(source), cell.metadata]); + return hashVal; + } +} + +export class NotebookEditorSimpleWorker implements IRequestHandler, IDisposable { + _requestHandlerBrand: any; + + private _models: { [uri: string]: MirrorNotebookDocument; }; + + constructor() { + this._models = Object.create(null); + } + dispose(): void { + } + + public acceptNewModel(uri: string, data: NotebookDataDto): void { + this._models[uri] = new MirrorNotebookDocument(URI.parse(uri), data.cells.map(dto => new MirrorCell( + (dto as unknown as IMainCellDto).handle, + dto.source, + dto.language, + dto.cellKind, + dto.outputs, + dto.metadata + )), data.languages, data.metadata); + } + + public acceptRemovedModel(strURL: string): void { + if (!this._models[strURL]) { + return; + } + delete this._models[strURL]; + } + + computeDiff(originalUrl: string, modifiedUrl: string): INotebookDiffResult { + const original = this._getModel(originalUrl); + const modified = this._getModel(modifiedUrl); + + const diff = new LcsDiff(new CellSequence(original), new CellSequence(modified)); + const diffResult = diff.ComputeDiff(false); + + /* let cellLineChanges: { originalCellhandle: number, modifiedCellhandle: number, lineChanges: editorCommon.ILineChange[] }[] = []; + + diffResult.changes.forEach(change => { + if (change.modifiedLength === 0) { + // deletion ... + return; + } + + if (change.originalLength === 0) { + // insertion + return; + } + + for (let i = 0, len = Math.min(change.modifiedLength, change.originalLength); i < len; i++) { + let originalIndex = change.originalStart + i; + let modifiedIndex = change.modifiedStart + i; + + const originalCell = original.cells[originalIndex]; + const modifiedCell = modified.cells[modifiedIndex]; + + if (originalCell.getValue() !== modifiedCell.getValue()) { + // console.log(`original cell ${originalIndex} content change`); + const originalLines = originalCell.textBuffer.getLinesContent(); + const modifiedLines = modifiedCell.textBuffer.getLinesContent(); + const diffComputer = new DiffComputer(originalLines, modifiedLines, { + shouldComputeCharChanges: true, + shouldPostProcessCharChanges: true, + shouldIgnoreTrimWhitespace: false, + shouldMakePrettyDiff: true, + maxComputationTime: 5000 + }); + + const lineChanges = diffComputer.computeDiff().changes; + + cellLineChanges.push({ + originalCellhandle: originalCell.handle, + modifiedCellhandle: modifiedCell.handle, + lineChanges + }); + + // console.log(lineDecorations); + + } else { + // console.log(`original cell ${originalIndex} metadata change`); + } + + } + }); + */ + return { + cellsDiff: diffResult, + // linesDiff: cellLineChanges + }; + } + + protected _getModel(uri: string): MirrorNotebookDocument { + return this._models[uri]; + } +} + +/** + * Called on the worker side + * @internal + */ +export function create(host: EditorWorkerHost): IRequestHandler { + return new NotebookEditorSimpleWorker(); +} + diff --git a/src/vs/workbench/contrib/notebook/common/services/notebookWorkerService.ts b/src/vs/workbench/contrib/notebook/common/services/notebookWorkerService.ts new file mode 100644 index 00000000000..f85fd6f5cc9 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/services/notebookWorkerService.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/base/common/uri'; +import { ILineChange } from 'vs/editor/common/editorCommon'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { INotebookDiffResult } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +export const ID_NOTEBOOK_EDITOR_WORKER_SERVICE = 'notebookEditorWorkerService'; +export const INotebookEditorWorkerService = createDecorator(ID_NOTEBOOK_EDITOR_WORKER_SERVICE); + +export interface IDiffComputationResult { + quitEarly: boolean; + identical: boolean; + changes: ILineChange[]; +} + +export interface INotebookEditorWorkerService { + readonly _serviceBrand: undefined; + + canComputeDiff(original: URI, modified: URI): boolean; + computeDiff(original: URI, modified: URI): Promise; +} diff --git a/src/vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl.ts new file mode 100644 index 00000000000..b209d8c7fd4 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl.ts @@ -0,0 +1,219 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { SimpleWorkerClient } from 'vs/base/common/worker/simpleWorker'; +import { DefaultWorkerFactory } from 'vs/base/worker/defaultWorkerFactory'; +import { INotebookDiffResult } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { NotebookEditorSimpleWorker } from 'vs/workbench/contrib/notebook/common/services/notebookSimpleWorker'; +import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerService'; + +export class NotebookEditorWorkerServiceImpl extends Disposable implements INotebookEditorWorkerService { + declare readonly _serviceBrand: undefined; + + private readonly _workerManager: WorkerManager; + + constructor( + @INotebookService notebookService: INotebookService + ) { + super(); + + this._workerManager = this._register(new WorkerManager(notebookService)); + } + canComputeDiff(original: URI, modified: URI): boolean { + throw new Error('Method not implemented.'); + } + + computeDiff(original: URI, modified: URI): Promise { + return this._workerManager.withWorker().then(client => { + return client.computeDiff(original, modified); + }); + } +} + +export class WorkerManager extends Disposable { + private _editorWorkerClient: NotebookWorkerClient | null; + // private _lastWorkerUsedTime: number; + + constructor( + private readonly _notebookService: INotebookService + ) { + super(); + this._editorWorkerClient = null; + // this._lastWorkerUsedTime = (new Date()).getTime(); + } + + withWorker(): Promise { + // this._lastWorkerUsedTime = (new Date()).getTime(); + if (!this._editorWorkerClient) { + this._editorWorkerClient = new NotebookWorkerClient(this._notebookService, 'notebookEditorWorkerService'); + } + return Promise.resolve(this._editorWorkerClient); + } +} + +export interface IWorkerClient { + getProxyObject(): Promise; + dispose(): void; +} + +export class NotebookEditorModelManager extends Disposable { + private _syncedModels: { [modelUrl: string]: IDisposable; } = Object.create(null); + private _syncedModelsLastUsedTime: { [modelUrl: string]: number; } = Object.create(null); + + constructor( + private readonly _proxy: NotebookEditorSimpleWorker, + private readonly _notebookService: INotebookService + ) { + super(); + } + + public ensureSyncedResources(resources: URI[]): void { + for (const resource of resources) { + let resourceStr = resource.toString(); + + if (!this._syncedModels[resourceStr]) { + this._beginModelSync(resource); + } + if (this._syncedModels[resourceStr]) { + this._syncedModelsLastUsedTime[resourceStr] = (new Date()).getTime(); + } + } + } + + private _beginModelSync(resource: URI): void { + let model = this._notebookService.listNotebookDocuments().find(document => document.uri.toString() === resource.toString()); + if (!model) { + return; + } + + let modelUrl = resource.toString(); + + this._proxy.acceptNewModel( + model.uri.toString(), + { + cells: model.cells.map(cell => ({ + handle: cell.handle, + uri: cell.uri, + source: cell.getValue(), + eol: cell.textBuffer.getEOL(), + language: cell.language, + cellKind: cell.cellKind, + outputs: cell.outputs, + metadata: cell.metadata + })), + languages: model.languages, + metadata: model.metadata + } + ); + + const toDispose = new DisposableStore(); + + // TODO, accept Model change + + // toDispose.add(model.onDidChangeContent((e) => { + // this._proxy.acceptModelChanged(modelUrl.toString(), e); + // })); + toDispose.add(model.onWillDispose(() => { + this._stopModelSync(modelUrl); + })); + toDispose.add(toDisposable(() => { + this._proxy.acceptRemovedModel(modelUrl); + })); + + this._syncedModels[modelUrl] = toDispose; + } + + private _stopModelSync(modelUrl: string): void { + let toDispose = this._syncedModels[modelUrl]; + delete this._syncedModels[modelUrl]; + delete this._syncedModelsLastUsedTime[modelUrl]; + dispose(toDispose); + } +} + +export class EditorWorkerHost { + + private readonly _workerClient: NotebookWorkerClient; + + constructor(workerClient: NotebookWorkerClient) { + this._workerClient = workerClient; + } + + // foreign host request + public fhr(method: string, args: any[]): Promise { + return this._workerClient.fhr(method, args); + } +} + +export class NotebookWorkerClient extends Disposable { + private _worker: IWorkerClient | null; + private readonly _workerFactory: DefaultWorkerFactory; + private _modelManager: NotebookEditorModelManager | null; + + + constructor(private readonly _notebookService: INotebookService, label: string) { + super(); + this._workerFactory = new DefaultWorkerFactory(label); + this._worker = null; + this._modelManager = null; + + } + + // foreign host request + public fhr(method: string, args: any[]): Promise { + throw new Error(`Not implemented!`); + } + + computeDiff(original: URI, modified: URI) { + return this._withSyncedResources([original, modified]).then(proxy => { + return proxy.computeDiff(original.toString(), modified.toString()); + }); + } + + private _getOrCreateModelManager(proxy: NotebookEditorSimpleWorker): NotebookEditorModelManager { + if (!this._modelManager) { + this._modelManager = this._register(new NotebookEditorModelManager(proxy, this._notebookService)); + } + return this._modelManager; + } + + protected _withSyncedResources(resources: URI[]): Promise { + return this._getProxy().then((proxy) => { + this._getOrCreateModelManager(proxy).ensureSyncedResources(resources); + return proxy; + }); + } + + private _getOrCreateWorker(): IWorkerClient { + if (!this._worker) { + try { + this._worker = this._register(new SimpleWorkerClient( + this._workerFactory, + 'vs/workbench/contrib/notebook/common/services/notebookSimpleWorker', + new EditorWorkerHost(this) + )); + } catch (err) { + // logOnceWebWorkerWarning(err); + // this._worker = new SynchronousWorkerClient(new EditorSimpleWorker(new EditorWorkerHost(this), null)); + throw (err); + } + } + return this._worker; + } + + protected _getProxy(): Promise { + return this._getOrCreateWorker().getProxyObject().then(undefined, (err) => { + // logOnceWebWorkerWarning(err); + // this._worker = new SynchronousWorkerClient(new EditorSimpleWorker(new EditorWorkerHost(this), null)); + // return this._getOrCreateWorker().getProxyObject(); + throw (err); + }); + } + + +} diff --git a/src/vs/workbench/contrib/notebook/electron-browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/electron-browser/notebook.contribution.ts index fe8d01653ca..d6e000d64cd 100644 --- a/src/vs/workbench/contrib/notebook/electron-browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/electron-browser/notebook.contribution.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isMacintosh } from 'vs/base/common/platform'; import { CopyAction, CutAction, PasteAction } from 'vs/editor/contrib/clipboard/clipboard'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { getActiveNotebookEditor } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; @@ -29,35 +28,34 @@ function getFocusedElectronBasedWebviewDelegate(accessor: ServicesAccessor): Ele return; } -if (isMacintosh) { - function withWebview(accessor: ServicesAccessor, f: (webviewe: ElectronWebviewBasedWebview) => void) { - const webview = getFocusedElectronBasedWebviewDelegate(accessor); - if (webview) { - f(webview); - return true; - } - return false; +function withWebview(accessor: ServicesAccessor, f: (webviewe: ElectronWebviewBasedWebview) => void) { + const webview = getFocusedElectronBasedWebviewDelegate(accessor); + if (webview) { + f(webview); + return true; } - - const PRIORITY = 100; - - UndoCommand.addImplementation(PRIORITY, accessor => { - return withWebview(accessor, webview => webview.undo()); - }); - - RedoCommand.addImplementation(PRIORITY, accessor => { - return withWebview(accessor, webview => webview.redo()); - }); - - CopyAction?.addImplementation(PRIORITY, accessor => { - return withWebview(accessor, webview => webview.copy()); - }); - - PasteAction?.addImplementation(PRIORITY, accessor => { - return withWebview(accessor, webview => webview.paste()); - }); - - CutAction?.addImplementation(PRIORITY, accessor => { - return withWebview(accessor, webview => webview.cut()); - }); + return false; } + +const PRIORITY = 100; + +UndoCommand.addImplementation(PRIORITY, accessor => { + return withWebview(accessor, webview => webview.undo()); +}); + +RedoCommand.addImplementation(PRIORITY, accessor => { + return withWebview(accessor, webview => webview.redo()); +}); + +CopyAction?.addImplementation(PRIORITY, accessor => { + return withWebview(accessor, webview => webview.copy()); +}); + +PasteAction?.addImplementation(PRIORITY, accessor => { + return withWebview(accessor, webview => webview.paste()); +}); + +CutAction?.addImplementation(PRIORITY, accessor => { + return withWebview(accessor, webview => webview.cut()); +}); + diff --git a/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts b/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts index d092389a66e..38a91a236ce 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts @@ -269,7 +269,7 @@ suite('NotebookCommon', () => { for (let i = 0; i < 5; i++) { cells.push( - new TestCell('notebook', i, [`var a = ${i};`], 'javascript', CellKind.Code, [], textModelService) + new TestCell('notebook', i, `var a = ${i};`, 'javascript', CellKind.Code, [], textModelService) ); } @@ -295,8 +295,8 @@ suite('NotebookCommon', () => { ] ); - const cellA = new TestCell('notebook', 6, ['var a = 6;'], 'javascript', CellKind.Code, [], textModelService); - const cellB = new TestCell('notebook', 7, ['var a = 7;'], 'javascript', CellKind.Code, [], textModelService); + const cellA = new TestCell('notebook', 6, 'var a = 6;', 'javascript', CellKind.Code, [], textModelService); + const cellB = new TestCell('notebook', 7, 'var a = 7;', 'javascript', CellKind.Code, [], textModelService); const modifiedCells = [ cells[0], diff --git a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts index 23ffd1fb439..010ddba6af2 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { CellKind, CellEditType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellEditType, CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { withTestNotebook, TestCell, setupInstantiationService } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; @@ -23,15 +23,15 @@ suite('NotebookTextModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }] ], (editor, viewModel, textModel) => { - textModel.$applyEdit(textModel.versionId, [ - { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [], textModelService)] }, - { editType: CellEditType.Insert, index: 3, cells: [new TestCell(viewModel.viewType, 6, ['var f = 6;'], 'javascript', CellKind.Code, [], textModelService)] }, + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(viewModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], textModelService)] }, + { editType: CellEditType.Replace, index: 3, count: 0, cells: [new TestCell(viewModel.viewType, 6, 'var f = 6;', 'javascript', CellKind.Code, [], textModelService)] }, ], true); assert.equal(textModel.cells.length, 6); @@ -48,15 +48,15 @@ suite('NotebookTextModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }] ], (editor, viewModel, textModel) => { - textModel.$applyEdit(textModel.versionId, [ - { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [], textModelService)] }, - { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 6, ['var f = 6;'], 'javascript', CellKind.Code, [], textModelService)] }, + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(viewModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], textModelService)] }, + { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(viewModel.viewType, 6, 'var f = 6;', 'javascript', CellKind.Code, [], textModelService)] }, ], true); assert.equal(textModel.cells.length, 6); @@ -73,15 +73,15 @@ suite('NotebookTextModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }] ], (editor, viewModel, textModel) => { - textModel.$applyEdit(textModel.versionId, [ - { editType: CellEditType.Delete, index: 1, count: 1 }, - { editType: CellEditType.Delete, index: 3, count: 1 }, + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Replace, index: 1, count: 1, cells: [] }, + { editType: CellEditType.Replace, index: 3, count: 1, cells: [] }, ], true); assert.equal(textModel.cells[0].getValue(), 'var a = 1;'); @@ -96,15 +96,15 @@ suite('NotebookTextModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }] ], (editor, viewModel, textModel) => { - textModel.$applyEdit(textModel.versionId, [ - { editType: CellEditType.Delete, index: 1, count: 1 }, - { editType: CellEditType.Insert, index: 3, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [], textModelService)] }, + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Replace, index: 1, count: 1, cells: [] }, + { editType: CellEditType.Replace, index: 3, count: 0, cells: [new TestCell(viewModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], textModelService)] }, ], true); assert.equal(textModel.cells.length, 4); @@ -121,15 +121,15 @@ suite('NotebookTextModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }] ], (editor, viewModel, textModel) => { - textModel.$applyEdit(textModel.versionId, [ - { editType: CellEditType.Delete, index: 1, count: 1 }, - { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [], textModelService)] }, + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Replace, index: 1, count: 1, cells: [] }, + { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(viewModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], textModelService)] }, ], true); assert.equal(textModel.cells.length, 4); @@ -139,5 +139,113 @@ suite('NotebookTextModel', () => { } ); }); -}); + test('(replace) delete + insert at same position', function () { + withTestNotebook( + instantiationService, + blukEditService, + undoRedoService, + [ + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }] + ], + (editor, viewModel, textModel) => { + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Replace, index: 1, count: 1, cells: [new TestCell(viewModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], textModelService)] }, + ], true); + + assert.equal(textModel.cells.length, 4); + assert.equal(textModel.cells[0].getValue(), 'var a = 1;'); + assert.equal(textModel.cells[1].getValue(), 'var e = 5;'); + assert.equal(textModel.cells[2].getValue(), 'var c = 3;'); + } + ); + }); + + test('output', function () { + withTestNotebook( + instantiationService, + blukEditService, + undoRedoService, + [ + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ], + (editor, viewModel, textModel) => { + + // invalid index 1 + assert.throws(() => { + textModel.applyEdit(textModel.versionId, [{ + index: Number.MAX_VALUE, + editType: CellEditType.Output, + outputs: [] + }], true); + }); + + // invalid index 2 + assert.throws(() => { + textModel.applyEdit(textModel.versionId, [{ + index: -1, + editType: CellEditType.Output, + outputs: [] + }], true); + }); + + textModel.applyEdit(textModel.versionId, [{ + index: 0, + editType: CellEditType.Output, + outputs: [{ + outputKind: CellOutputKind.Rich, + outputId: 'someId', + data: { 'text/markdown': '_Hello_' } + }] + }], true); + + assert.equal(textModel.cells.length, 1); + assert.equal(textModel.cells[0].outputs.length, 1); + assert.equal(textModel.cells[0].outputs[0].outputKind, CellOutputKind.Rich); + } + ); + }); + + test('metadata', function () { + withTestNotebook( + instantiationService, + blukEditService, + undoRedoService, + [ + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ], + (editor, viewModel, textModel) => { + + // invalid index 1 + assert.throws(() => { + textModel.applyEdit(textModel.versionId, [{ + index: Number.MAX_VALUE, + editType: CellEditType.Metadata, + metadata: { editable: false } + }], true); + }); + + // invalid index 2 + assert.throws(() => { + textModel.applyEdit(textModel.versionId, [{ + index: -1, + editType: CellEditType.Metadata, + metadata: { editable: false } + }], true); + }); + + textModel.applyEdit(textModel.versionId, [{ + index: 0, + editType: CellEditType.Metadata, + metadata: { editable: false }, + }], true); + + assert.equal(textModel.cells.length, 1); + assert.equal(textModel.cells[0].metadata?.editable, false); + } + ); + }); +}); diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index 60d7d3cfdf7..b45c690f1af 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -6,14 +6,14 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind, NotebookCellMetadata, diff } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, diff, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { withTestNotebook, TestCell, NotebookEditorTestModel, setupInstantiationService } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { TrackedRangeStickiness } from 'vs/editor/common/model'; -import { reduceCellRanges, ICellRange } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { reduceCellRanges } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; suite('NotebookViewModel', () => { @@ -36,14 +36,14 @@ suite('NotebookViewModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: false }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }] ], (editor, viewModel) => { assert.equal(viewModel.viewCells[0].metadata?.editable, true); assert.equal(viewModel.viewCells[1].metadata?.editable, false); - const cell = viewModel.insertCell(1, new TestCell(viewModel.viewType, 0, ['var c = 3;'], 'javascript', CellKind.Code, [], textModelService), true); + const cell = viewModel.insertCell(1, new TestCell(viewModel.viewType, 0, 'var c = 3;', 'javascript', CellKind.Code, [], textModelService), true); assert.equal(viewModel.viewCells.length, 3); assert.equal(viewModel.notebookDocument.cells.length, 3); assert.equal(viewModel.getCellIndex(cell), 1); @@ -62,23 +62,23 @@ suite('NotebookViewModel', () => { blukEditService, undoRedoService, [ - [['//a'], 'javascript', CellKind.Code, [], { editable: true }], - [['//b'], 'javascript', CellKind.Code, [], { editable: true }], - [['//c'], 'javascript', CellKind.Code, [], { editable: true }], + ['//a', 'javascript', CellKind.Code, [], { editable: true }], + ['//b', 'javascript', CellKind.Code, [], { editable: true }], + ['//c', 'javascript', CellKind.Code, [], { editable: true }], ], (editor, viewModel) => { - viewModel.moveCellToIdx(0, 0, false); + viewModel.moveCellToIdx(0, 1, 0, false); // no-op assert.equal(viewModel.viewCells[0].getText(), '//a'); assert.equal(viewModel.viewCells[1].getText(), '//b'); - viewModel.moveCellToIdx(0, 1, false); + viewModel.moveCellToIdx(0, 1, 1, false); // b, a, c assert.equal(viewModel.viewCells[0].getText(), '//b'); assert.equal(viewModel.viewCells[1].getText(), '//a'); assert.equal(viewModel.viewCells[2].getText(), '//c'); - viewModel.moveCellToIdx(0, 2, false); + viewModel.moveCellToIdx(0, 1, 2, false); // a, c, b assert.equal(viewModel.viewCells[0].getText(), '//a'); assert.equal(viewModel.viewCells[1].getText(), '//c'); @@ -93,17 +93,17 @@ suite('NotebookViewModel', () => { blukEditService, undoRedoService, [ - [['//a'], 'javascript', CellKind.Code, [], { editable: true }], - [['//b'], 'javascript', CellKind.Code, [], { editable: true }], - [['//c'], 'javascript', CellKind.Code, [], { editable: true }], + ['//a', 'javascript', CellKind.Code, [], { editable: true }], + ['//b', 'javascript', CellKind.Code, [], { editable: true }], + ['//c', 'javascript', CellKind.Code, [], { editable: true }], ], (editor, viewModel) => { - viewModel.moveCellToIdx(1, 0, false); + viewModel.moveCellToIdx(1, 1, 0, false); // b, a, c assert.equal(viewModel.viewCells[0].getText(), '//b'); assert.equal(viewModel.viewCells[1].getText(), '//a'); - viewModel.moveCellToIdx(2, 0, false); + viewModel.moveCellToIdx(2, 1, 0, false); // c, b, a assert.equal(viewModel.viewCells[0].getText(), '//c'); assert.equal(viewModel.viewCells[1].getText(), '//b'); @@ -118,21 +118,21 @@ suite('NotebookViewModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: true }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: true }] ], (editor, viewModel) => { const firstViewCell = viewModel.viewCells[0]; const lastViewCell = viewModel.viewCells[viewModel.viewCells.length - 1]; const insertIndex = viewModel.getCellIndex(firstViewCell) + 1; - const cell = viewModel.insertCell(insertIndex, new TestCell(viewModel.viewType, 3, ['var c = 3;'], 'javascript', CellKind.Code, [], textModelService), true); + const cell = viewModel.insertCell(insertIndex, new TestCell(viewModel.viewType, 3, 'var c = 3;', 'javascript', CellKind.Code, [], textModelService), true); const addedCellIndex = viewModel.getCellIndex(cell); viewModel.deleteCell(addedCellIndex, true); const secondInsertIndex = viewModel.getCellIndex(lastViewCell) + 1; - const cell2 = viewModel.insertCell(secondInsertIndex, new TestCell(viewModel.viewType, 4, ['var d = 4;'], 'javascript', CellKind.Code, [], textModelService), true); + const cell2 = viewModel.insertCell(secondInsertIndex, new TestCell(viewModel.viewType, 4, 'var d = 4;', 'javascript', CellKind.Code, [], textModelService), true); assert.equal(viewModel.viewCells.length, 3); assert.equal(viewModel.notebookDocument.cells.length, 3); @@ -147,11 +147,11 @@ suite('NotebookViewModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], {}], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: true, runnable: true }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: true, runnable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false, runnable: true }], - [['var e = 5;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + ['var a = 1;', 'javascript', CellKind.Code, [], {}], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: true, runnable: true }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: true, runnable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false, runnable: true }], + ['var e = 5;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], ], (editor, viewModel) => { viewModel.notebookDocument.metadata = { editable: true, runnable: true, cellRunnable: true, cellEditable: true, cellHasExecutionOrder: true }; @@ -269,11 +269,11 @@ suite('NotebookViewModel Decorations', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], {}], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: true, runnable: true }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: true, runnable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false, runnable: true }], - [['var e = 5;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + ['var a = 1;', 'javascript', CellKind.Code, [], {}], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: true, runnable: true }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: true, runnable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false, runnable: true }], + ['var e = 5;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], ], (editor, viewModel) => { const trackedId = viewModel.setTrackedRange('test', { start: 1, end: 2 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter); @@ -283,7 +283,7 @@ suite('NotebookViewModel Decorations', () => { end: 2, }); - viewModel.insertCell(0, new TestCell(viewModel.viewType, 5, ['var d = 6;'], 'javascript', CellKind.Code, [], textModelService), true); + viewModel.insertCell(0, new TestCell(viewModel.viewType, 5, 'var d = 6;', 'javascript', CellKind.Code, [], textModelService), true); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 2, @@ -297,7 +297,7 @@ suite('NotebookViewModel Decorations', () => { end: 2 }); - viewModel.insertCell(3, new TestCell(viewModel.viewType, 6, ['var d = 7;'], 'javascript', CellKind.Code, [], textModelService), true); + viewModel.insertCell(3, new TestCell(viewModel.viewType, 6, 'var d = 7;', 'javascript', CellKind.Code, [], textModelService), true); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 1, @@ -327,13 +327,13 @@ suite('NotebookViewModel Decorations', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], {}], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: true, runnable: true }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: true, runnable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false, runnable: true }], - [['var e = 5;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], - [['var e = 6;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], - [['var e = 7;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + ['var a = 1;', 'javascript', CellKind.Code, [], {}], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: true, runnable: true }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: true, runnable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false, runnable: true }], + ['var e = 5;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + ['var e = 6;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + ['var e = 7;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], ], (editor, viewModel) => { const trackedId = viewModel.setTrackedRange('test', { start: 1, end: 3 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter); @@ -343,14 +343,14 @@ suite('NotebookViewModel Decorations', () => { end: 3 }); - viewModel.insertCell(5, new TestCell(viewModel.viewType, 8, ['var d = 9;'], 'javascript', CellKind.Code, [], textModelService), true); + viewModel.insertCell(5, new TestCell(viewModel.viewType, 8, 'var d = 9;', 'javascript', CellKind.Code, [], textModelService), true); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 1, end: 3 }); - viewModel.insertCell(4, new TestCell(viewModel.viewType, 9, ['var d = 10;'], 'javascript', CellKind.Code, [], textModelService), true); + viewModel.insertCell(4, new TestCell(viewModel.viewType, 9, 'var d = 10;', 'javascript', CellKind.Code, [], textModelService), true); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 1, diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index a79ceb86034..e21f25b2b02 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -12,13 +12,13 @@ import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { Range } from 'vs/editor/common/core/range'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { EditorModel } from 'vs/workbench/common/editor'; -import { ICellRange, ICellViewModel, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICellViewModel, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo, INotebookDeltaDecoration, INotebookEditorCreationOptions, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellKind, CellUri, INotebookEditorModel, IProcessedOutput, NotebookCellMetadata, INotebookKernelInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellUri, INotebookEditorModel, IProcessedOutput, NotebookCellMetadata, IInsetRenderOutput, ICellRange, INotebookKernelInfo2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { ICompositeCodeEditor, IEditor } from 'vs/editor/common/editorCommon'; import { NotImplementedError } from 'vs/base/common/errors'; @@ -33,22 +33,24 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { ScrollEvent } from 'vs/base/common/scrollable'; export class TestCell extends NotebookCellTextModel { constructor( public viewType: string, handle: number, - public source: string[], + public source: string, language: string, cellKind: CellKind, outputs: IProcessedOutput[], modelService: ITextModelService ) { - super(CellUri.generate(URI.parse('test:///fake/notebook'), handle), handle, source, language, cellKind, outputs, undefined, modelService); + super(CellUri.generate(URI.parse('test:///fake/notebook'), handle), handle, source, language, cellKind, outputs, undefined, { transientMetadata: {}, transientOutputs: false }, modelService); } } export class TestNotebookEditor implements INotebookEditor { + isEmbedded = false; private _isDisposed = false; get isDisposed() { @@ -58,18 +60,27 @@ export class TestNotebookEditor implements INotebookEditor { get viewModel() { return undefined; } + creationOptions: INotebookEditorCreationOptions = { isEmbedded: false }; constructor( ) { } + + setOptions(options: NotebookEditorOptions | undefined): Promise { + throw new Error('Method not implemented.'); + } + hideInset(output: IProcessedOutput): void { throw new Error('Method not implemented.'); } multipleKernelsAvailable: boolean = false; onDidChangeAvailableKernels: Event = new Emitter().event; - - + onDidChangeActiveCell: Event = new Emitter().event; + onDidScroll = new Emitter().event; + onWillDispose = new Emitter().event; + onDidChangeVisibleRanges: Event = new Emitter().event; + visibleRanges: ICellRange[] = []; uri?: URI | undefined; textModel?: NotebookTextModel | undefined; @@ -95,7 +106,7 @@ export class TestNotebookEditor implements INotebookEditor { } cursorNavigationMode = false; - activeKernel: INotebookKernelInfo | undefined; + activeKernel: INotebookKernelInfo2 | undefined; onDidChangeKernel: Event = new Emitter().event; onDidChangeActiveEditor: Event = new Emitter().event; activeCodeEditor: IEditor | undefined; @@ -177,7 +188,7 @@ export class TestNotebookEditor implements INotebookEditor { throw new Error('Method not implemented.'); } - moveCellToIdx(cell: ICellViewModel, index: number): Promise { + async moveCellsToIdx(index: number, length: number, toIdx: number): Promise { throw new Error('Method not implemented.'); } @@ -253,7 +264,7 @@ export class TestNotebookEditor implements INotebookEditor { // throw new Error('Method not implemented.'); return; } - createInset(cell: CellViewModel, output: IProcessedOutput, shadowContent: string, offset: number): Promise { + createInset(cell: CellViewModel, output: IInsetRenderOutput, offset: number): Promise { return Promise.resolve(); } removeInset(output: IProcessedOutput): void { @@ -275,6 +286,10 @@ export class TestNotebookEditor implements INotebookEditor { throw new Error('Method not implemented.'); } + deltaCellDecorations(oldDecorations: string[], newDecorations: INotebookDeltaDecoration[]): string[] { + throw new Error('Method not implemented.'); + } + deltaCellOutputContainerClassNames(cellId: string, added: string[], removed: string[]): void { throw new Error('Method not implemented.'); } @@ -369,15 +384,21 @@ export function setupInstantiationService() { return instantiationService; } -export function withTestNotebook(instantiationService: TestInstantiationService, blukEditService: IBulkEditService, undoRedoService: IUndoRedoService, cells: [string[], string, CellKind, IProcessedOutput[], NotebookCellMetadata][], callback: (editor: TestNotebookEditor, viewModel: NotebookViewModel, textModel: NotebookTextModel) => void) { +export function withTestNotebook(instantiationService: TestInstantiationService, blukEditService: IBulkEditService, undoRedoService: IUndoRedoService, cells: [string, string, CellKind, IProcessedOutput[], NotebookCellMetadata][], callback: (editor: TestNotebookEditor, viewModel: NotebookViewModel, textModel: NotebookTextModel) => void) { const textModelService = instantiationService.get(ITextModelService); const viewType = 'notebook'; const editor = new TestNotebookEditor(); const notebook = new NotebookTextModel(0, viewType, false, URI.parse('test'), undoRedoService, textModelService); - notebook.cells = cells.map((cell, index) => { - return new NotebookCellTextModel(notebook.uri, index, cell[0], cell[1], cell[2], cell[3], cell[4], textModelService); - }); + notebook.initialize(cells.map(cell => { + return { + source: cell[0], + language: cell[1], + cellKind: cell[2], + outputs: cell[3], + metadata: cell[4] + }; + })); const model = new NotebookEditorTestModel(notebook); const eventDispatcher = new NotebookEventDispatcher(); const viewModel = new NotebookViewModel(viewType, model.notebook, eventDispatcher, null, instantiationService, blukEditService, undoRedoService); diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index ad427f0da42..472df16c4f5 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -115,7 +115,10 @@ registerAction2(class extends Action2 { }); } async run(accessor: ServicesAccessor, channelId: string): Promise { - accessor.get(IOutputService).showChannel(channelId); + if (typeof channelId === 'string') { + // Sometimes the action is executed with no channelId parameter, then we should just ignore it #103496 + accessor.get(IOutputService).showChannel(channelId); + } } }); registerAction2(class extends Action2 { @@ -319,7 +322,7 @@ Registry.as(ConfigurationExtensions.Configuration).regis type: 'boolean', description: nls.localize('output.smartScroll.enabled', "Enable/disable the ability of smart scrolling in the output view. Smart scrolling allows you to lock scrolling automatically when you click in the output view and unlocks when you click in the last line."), default: true, - scope: ConfigurationScope.APPLICATION, + scope: ConfigurationScope.WINDOW, tags: ['output'] } } diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index 2e5bd2cc7eb..337746000d6 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -13,7 +13,7 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/tex import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { AbstractTextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; import { OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_ACTIVE_LOG_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK } from 'vs/workbench/contrib/output/common/output'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; @@ -146,7 +146,7 @@ export class OutputViewPane extends ViewPane { this.channelId = channel.id; const descriptor = this.outputService.getChannelDescriptor(channel.id); CONTEXT_ACTIVE_LOG_OUTPUT.bindTo(this.contextKeyService).set(!!descriptor?.file && descriptor?.log); - this.editorPromise = this.editor.setInput(this.createInput(channel), EditorOptions.create({ preserveFocus: true }), CancellationToken.None) + this.editorPromise = this.editor.setInput(this.createInput(channel), EditorOptions.create({ preserveFocus: true }), Object.create(null), CancellationToken.None) .then(() => this.editor); } @@ -228,7 +228,7 @@ export class OutputEditor extends AbstractTextResourceEditor { return channel ? nls.localize('outputViewWithInputAriaLabel', "{0}, Output panel", channel.label) : nls.localize('outputViewAriaLabel', "Output panel"); } - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const focus = !(options && options.preserveFocus); if (input.matches(this.input)) { return; @@ -238,7 +238,7 @@ export class OutputEditor extends AbstractTextResourceEditor { // Dispose previous input (Output panel is not a workbench editor) this.input.dispose(); } - await super.setInput(input, options, token); + await super.setInput(input, options, context, token); if (focus) { this.focus(); } diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index fc4533b84ea..fc27aa4c3ba 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -14,8 +14,8 @@ import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlighte import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; import { IAction, Action, Separator } from 'vs/base/common/actions'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorOptions } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { KeybindingsEditorModel, IKeybindingItemEntry, IListEntry, KEYBINDING_ENTRY_TEMPLATE_ID } from 'vs/workbench/services/preferences/common/keybindingsEditorModel'; @@ -58,7 +58,7 @@ interface ColumnItem { const oddRowBackgroundColor = new Color(new RGBA(130, 130, 130, 0.04)); -export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditorPane { +export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorPane { static readonly ID: string = 'workbench.editor.keybindings'; @@ -138,9 +138,9 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditorP this.createBody(keybindingsEditorElement); } - setInput(input: KeybindingsEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + setInput(input: KeybindingsEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this.keybindingsEditorContextKey.set(true); - return super.setInput(input, options, token) + return super.setInput(input, options, context, token) .then(() => this.render(!!(options && options.preserveFocus))); } diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 02f464009af..8b5017293dc 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -175,14 +175,14 @@ .settings-editor > .settings-body .settings-tree-container .setting-toolbar-container { position: absolute; - left: -32px; + left: -22px; top: 11px; bottom: 0px; width: 26px; } .settings-editor > .settings-body .settings-tree-container .monaco-list-row .mouseover .setting-toolbar-container > .monaco-toolbar .codicon, -.settings-editor > .settings-body .settings-tree-container .monaco-list-row .setting-item-contents.focused .setting-toolbar-container > .monaco-toolbar .codicon, +.settings-editor > .settings-body .settings-tree-container .monaco-list-row.focused .setting-item-contents .setting-toolbar-container > .monaco-toolbar .codicon, .settings-editor > .settings-body .settings-tree-container .monaco-list-row .setting-toolbar-container:hover > .monaco-toolbar .codicon, .settings-editor > .settings-body .settings-tree-container .monaco-list-row .setting-toolbar-container > .monaco-toolbar .active .codicon { opacity: 1; @@ -283,15 +283,34 @@ max-width: 1000px; margin: auto; box-sizing: border-box; - padding-left: 219px; - padding-right: 20px; + padding-left: 204px; + padding-right: 5px; overflow: visible; } +.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label::before, +.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label::after, +.settings-editor > .settings-body > .settings-tree-container .setting-item-contents::before, +.settings-editor > .settings-body > .settings-tree-container .setting-item-contents::after { + content: ' '; + position: absolute; + left: 0px; + right: 0px; +} + +.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label::before, +.settings-editor > .settings-body > .settings-tree-container .setting-item-contents::before { + top: 0px; +} + +.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label::after, +.settings-editor > .settings-body > .settings-tree-container .setting-item-contents::after { + bottom: 0px; +} + .settings-editor > .settings-body > .settings-tree-container .setting-item-contents { position: relative; - padding-top: 12px; - padding-bottom: 18px; + padding: 12px 15px 18px; white-space: normal; } @@ -299,11 +318,9 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - display: inline-block; - /* size to contents for hover to show context button */ + display: inline-block; /* size to contents for hover to show context button */ } - .settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-modified-indicator { display: none; } @@ -315,7 +332,7 @@ width: 6px; border-left-width: 2px; border-left-style: solid; - left: -9px; + left: 5px; top: 15px; bottom: 16px; } @@ -528,12 +545,18 @@ } .settings-editor > .settings-body > .settings-tree-container .settings-group-title-label { + display: inline-block; margin: 0px; font-weight: 600; + height: 100%; + box-sizing: border-box; + padding: 10px; + padding-left: 15px; + width: 100%; + position: relative; } .settings-editor > .settings-body > .settings-tree-container .settings-group-level-1 { - padding-top: 23px; font-size: 24px; } @@ -542,10 +565,6 @@ font-size: 20px; } -.settings-editor > .settings-body > .settings-tree-container .settings-group-level-1.settings-group-first { - padding-top: 7px; -} - .settings-editor.search-mode > .settings-body .settings-toc-container .monaco-list-row .settings-toc-count { display: block; } diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 5bfbe8133ab..42a9848038a 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -12,7 +12,7 @@ import * as nls from 'vs/nls'; import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IsMacNativeContext } from 'vs/platform/contextkey/common/contextkeys'; +import { InputFocusedContext, IsMacNativeContext } from 'vs/platform/contextkey/common/contextkeys'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -40,6 +40,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { DefaultPreferencesEditorInput, KeybindingsEditorInput, PreferencesEditorInput, SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { AbstractSideBySideEditorInputFactory } from 'vs/workbench/browser/parts/editor/editor.contribution'; +import { WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; const SETTINGS_EDITOR_COMMAND_SEARCH = 'settings.action.search'; @@ -50,6 +51,8 @@ const SETTINGS_EDITOR_COMMAND_EDIT_FOCUSED_SETTING = 'settings.action.editFocuse const SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_FROM_SEARCH = 'settings.action.focusSettingsFromSearch'; const SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_LIST = 'settings.action.focusSettingsList'; const SETTINGS_EDITOR_COMMAND_FOCUS_TOC = 'settings.action.focusTOC'; +const SETTINGS_EDITOR_COMMAND_FOCUS_TOC2 = 'settings.action.focusTOC2'; +const SETTINGS_EDITOR_COMMAND_FOCUS_CONTROL = 'settings.action.focusSettingControl'; const SETTINGS_EDITOR_COMMAND_SWITCH_TO_JSON = 'settings.switchToJSON'; const SETTINGS_EDITOR_COMMAND_FILTER_MODIFIED = 'settings.filterByModified'; @@ -507,6 +510,14 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon } return null; } + + function settingsEditorFocusSearch(accessor: ServicesAccessor) { + const preferencesEditor = getPreferencesEditor(accessor); + if (preferencesEditor) { + preferencesEditor.focusSearch(); + } + } + registerAction2(class extends Action2 { constructor() { super({ @@ -521,12 +532,24 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon }); } - run(accessor: ServicesAccessor) { - const preferencesEditor = getPreferencesEditor(accessor); - if (preferencesEditor) { - preferencesEditor.focusSearch(); - } + run(accessor: ServicesAccessor) { settingsEditorFocusSearch(accessor); } + }); + + registerAction2(class extends Action2 { + constructor() { + super({ + id: SETTINGS_EDITOR_COMMAND_SEARCH, + precondition: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_TOC_ROW_FOCUS), + keybinding: { + primary: KeyCode.Escape, + weight: KeybindingWeight.WorkbenchContrib, + when: null + }, + title: nls.localize('settings.focusSearch', "Focus settings search") + }); } + + run(accessor: ServicesAccessor) { settingsEditorFocusSearch(accessor); } }); registerAction2(class extends Action2 { @@ -691,16 +714,76 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon constructor() { super({ id: SETTINGS_EDITOR_COMMAND_FOCUS_TOC, - precondition: CONTEXT_SETTINGS_EDITOR, + keybinding: [ + { + primary: KeyCode.Escape, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_TOC_ROW_FOCUS.negate()), + }, + { + primary: KeyCode.LeftArrow, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_TOC_ROW_FOCUS.negate(), InputFocusedContext.negate()) + }], title: nls.localize('settings.focusSettingsTOC', "Focus settings TOC tree") }); } run(accessor: ServicesAccessor): void { const preferencesEditor = getPreferencesEditor(accessor); - if (preferencesEditor instanceof SettingsEditor2) { - preferencesEditor.focusTOC(); + if (!(preferencesEditor instanceof SettingsEditor2)) { + return; } + + if (document.activeElement?.classList.contains('monaco-list')) { + preferencesEditor.focusTOC(); + } else { + preferencesEditor.focusSettings(); + } + } + }); + + registerAction2(class extends Action2 { + constructor() { + super({ + id: SETTINGS_EDITOR_COMMAND_FOCUS_CONTROL, + precondition: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_TOC_ROW_FOCUS.negate(), WorkbenchListFocusContextKey), + keybinding: { + primary: KeyCode.Enter, + weight: KeybindingWeight.WorkbenchContrib, + }, + title: nls.localize('settings.focusSettingControl', "Focus setting control") + }); + } + + run(accessor: ServicesAccessor): void { + const preferencesEditor = getPreferencesEditor(accessor); + if (!(preferencesEditor instanceof SettingsEditor2)) { + return; + } + + if (document.activeElement?.classList.contains('monaco-list')) { + preferencesEditor.focusSettings(true); + } + } + }); + + registerAction2(class extends Action2 { + constructor() { + super({ + id: SETTINGS_EDITOR_COMMAND_FOCUS_TOC2, + + title: nls.localize('settings.focusSettingsTOC', "Focus settings TOC tree") + }); + } + + run(accessor: ServicesAccessor): void { + const preferencesEditor = getPreferencesEditor(accessor); + if (!(preferencesEditor instanceof SettingsEditor2)) { + return; + } + + preferencesEditor.focusTOC(); } }); diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts index b0401d43adc..e8bb2984293 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts @@ -40,9 +40,9 @@ import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; -import { EditorInput, EditorOptions, IEditorControl } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorControl, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; import { DefaultSettingsRenderer, FolderSettingsRenderer, IPreferencesRenderer, UserSettingsRenderer, WorkspaceSettingsRenderer } from 'vs/workbench/contrib/preferences/browser/preferencesRenderers'; import { SearchWidget, SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; @@ -53,7 +53,7 @@ import { IFilterResult, IPreferencesService, ISetting, ISettingsEditorModel, ISe import { DefaultPreferencesEditorInput, PreferencesEditorInput } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { DefaultSettingsEditorModel, SettingsEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; -export class PreferencesEditor extends BaseEditor { +export class PreferencesEditor extends EditorPane { static readonly ID: string = 'workbench.editor.preferencesEditor'; @@ -75,7 +75,7 @@ export class PreferencesEditor extends BaseEditor { get minimumWidth(): number { return this.sideBySidePreferencesWidget ? this.sideBySidePreferencesWidget.minimumWidth : 0; } get maximumWidth(): number { return this.sideBySidePreferencesWidget ? this.sideBySidePreferencesWidget.maximumWidth : Number.POSITIVE_INFINITY; } - // these setters need to exist because this extends from BaseEditor + // these setters need to exist because this extends from EditorPane set minimumWidth(value: number) { /*noop*/ } set maximumWidth(value: number) { /*noop*/ } @@ -151,14 +151,14 @@ export class PreferencesEditor extends BaseEditor { this.preferencesRenderers.editFocusedPreference(); } - setInput(newInput: EditorInput, options: SettingsEditorOptions | undefined, token: CancellationToken): Promise { + setInput(newInput: EditorInput, options: SettingsEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this.defaultSettingsEditorContextKey.set(true); this.defaultSettingsJSONEditorContextKey.set(true); if (options && options.query) { this.focusSearch(options.query); } - return super.setInput(newInput, options, token).then(() => this.updateInput(newInput as PreferencesEditorInput, options, token)); + return super.setInput(newInput, options, context, token).then(() => this.updateInput(newInput as PreferencesEditorInput, options, context, token)); } layout(dimension: DOM.Dimension): void { @@ -204,8 +204,8 @@ export class PreferencesEditor extends BaseEditor { super.setEditorVisible(visible, group); } - private updateInput(newInput: PreferencesEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - return this.sideBySidePreferencesWidget.setInput(newInput.secondary, newInput.primary, options, token).then(({ defaultPreferencesRenderer, editablePreferencesRenderer }) => { + private updateInput(newInput: PreferencesEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + return this.sideBySidePreferencesWidget.setInput(newInput.secondary, newInput.primary, options, context, token).then(({ defaultPreferencesRenderer, editablePreferencesRenderer }) => { if (token.isCancellationRequested) { return; } @@ -762,7 +762,7 @@ class SideBySidePreferencesWidget extends Widget { private defaultPreferencesHeader: HTMLElement; private defaultPreferencesEditor: DefaultPreferencesEditor; - private editablePreferencesEditor: BaseEditor | null = null; + private editablePreferencesEditor: EditorPane | null = null; private defaultPreferencesEditorContainer: HTMLElement; private editablePreferencesEditorContainer: HTMLElement; @@ -837,12 +837,12 @@ class SideBySidePreferencesWidget extends Widget { this._register(focusTracker.onDidFocus(() => this._onFocus.fire())); } - setInput(defaultPreferencesEditorInput: DefaultPreferencesEditorInput, editablePreferencesEditorInput: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise<{ defaultPreferencesRenderer?: IPreferencesRenderer, editablePreferencesRenderer?: IPreferencesRenderer; }> { + setInput(defaultPreferencesEditorInput: DefaultPreferencesEditorInput, editablePreferencesEditorInput: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<{ defaultPreferencesRenderer?: IPreferencesRenderer, editablePreferencesRenderer?: IPreferencesRenderer; }> { this.getOrCreateEditablePreferencesEditor(editablePreferencesEditorInput); this.settingsTargetsWidget.settingsTarget = this.getSettingsTarget(editablePreferencesEditorInput.resource!); return Promise.all([ - this.updateInput(this.defaultPreferencesEditor, defaultPreferencesEditorInput, DefaultSettingsEditorContribution.ID, editablePreferencesEditorInput.resource!, options, token), - this.updateInput(this.editablePreferencesEditor!, editablePreferencesEditorInput, SettingsEditorContribution.ID, defaultPreferencesEditorInput.resource!, options, token) + this.updateInput(this.defaultPreferencesEditor, defaultPreferencesEditorInput, DefaultSettingsEditorContribution.ID, editablePreferencesEditorInput.resource!, options, context, token), + this.updateInput(this.editablePreferencesEditor!, editablePreferencesEditorInput, SettingsEditorContribution.ID, defaultPreferencesEditorInput.resource!, options, context, token) ]) .then(([defaultPreferencesRenderer, editablePreferencesRenderer]) => { if (token.isCancellationRequested) { @@ -906,7 +906,7 @@ class SideBySidePreferencesWidget extends Widget { } } - private getOrCreateEditablePreferencesEditor(editorInput: EditorInput): BaseEditor { + private getOrCreateEditablePreferencesEditor(editorInput: EditorInput): EditorPane { if (this.editablePreferencesEditor) { return this.editablePreferencesEditor; } @@ -920,8 +920,8 @@ class SideBySidePreferencesWidget extends Widget { return editor; } - private updateInput(editor: BaseEditor, input: EditorInput, editorContributionId: string, associatedPreferencesModelUri: URI, options: EditorOptions | undefined, token: CancellationToken): Promise | undefined> { - return editor.setInput(input, options, token) + private updateInput(editor: EditorPane, input: EditorInput, editorContributionId: string, associatedPreferencesModelUri: URI, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise | undefined> { + return editor.setInput(input, options, context, token) .then(() => { if (token.isCancellationRequested) { return undefined; @@ -1025,8 +1025,8 @@ export class DefaultPreferencesEditor extends BaseTextEditor { return options; } - setInput(input: DefaultPreferencesEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - return super.setInput(input, options, token) + setInput(input: DefaultPreferencesEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + return super.setInput(input, options, context, token) .then(() => this.input!.resolve() .then(editorModel => { if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index a31a0fa0f9f..77b764417e7 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -39,12 +39,12 @@ import { attachButtonStyler, attachStylerCallback } from 'vs/platform/theme/comm import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { IUserDataAutoSyncService, IUserDataSyncService, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { IEditorMemento, IEditorPane } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { IEditorMemento, IEditorOpenContext, IEditorPane } from 'vs/workbench/common/editor'; import { attachSuggestEnabledInputBoxStyler, SuggestEnabledInput } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput'; import { SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; import { commonlyUsedData, tocData } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; -import { AbstractSettingRenderer, ISettingLinkClickEvent, ISettingOverrideClickEvent, resolveExtensionsSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers } from 'vs/workbench/contrib/preferences/browser/settingsTree'; +import { AbstractSettingRenderer, ISettingLinkClickEvent, ISettingOverrideClickEvent, resolveExtensionsSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers, updateSettingTreeTabOrder } from 'vs/workbench/contrib/preferences/browser/settingsTree'; import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; import { settingsTextInputBorder } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; import { createTOCIterator, TOCTree, TOCTreeModel } from 'vs/workbench/contrib/preferences/browser/tocTree'; @@ -76,7 +76,7 @@ const searchBoxLabel = localize('SearchSettings.AriaLabel', "Search settings"); const SETTINGS_AUTOSAVE_NOTIFIED_KEY = 'hasNotifiedOfSettingsAutosave'; const SETTINGS_EDITOR_STATE_KEY = 'settingsEditorState'; -export class SettingsEditor2 extends BaseEditor { +export class SettingsEditor2 extends EditorPane { static readonly ID: string = 'workbench.editor.settings2'; private static NUM_INSTANCES: number = 0; @@ -150,6 +150,7 @@ export class SettingsEditor2 extends BaseEditor { private editorMemento: IEditorMemento; private tocFocusedElement: SettingsTreeGroupElement | null = null; + private treeFocusedElement: SettingsTreeElement | null = null; private settingsTreeScrollTop = 0; private dimension!: DOM.Dimension; @@ -201,7 +202,7 @@ export class SettingsEditor2 extends BaseEditor { get minimumWidth(): number { return 375; } get maximumWidth(): number { return Number.POSITIVE_INFINITY; } - // these setters need to exist because this extends from BaseEditor + // these setters need to exist because this extends from EditorPane set minimumWidth(value: number) { /*noop*/ } set maximumWidth(value: number) { /*noop*/ } @@ -234,9 +235,9 @@ export class SettingsEditor2 extends BaseEditor { this.updateStyles(); } - setInput(input: SettingsEditor2Input, options: SettingsEditorOptions | undefined, token: CancellationToken): Promise { + setInput(input: SettingsEditor2Input, options: SettingsEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this.inSettingsEditorContextKey.set(true); - return super.setInput(input, options, token) + return super.setInput(input, options, context, token) .then(() => timeout(0)) // Force setInput to be async .then(() => { // Don't block setInput on render (which can trigger an async search) @@ -347,7 +348,8 @@ export class SettingsEditor2 extends BaseEditor { } } - focusSettings(): void { + focusSettings(focusSettingInput = false): void { + // TODO@roblourens is this in the right place? // Update ARIA global labels const labelElement = this.settingsAriaExtraLabelsContainer.querySelector('#settings_aria_more_actions_shortcut_label'); if (labelElement) { @@ -357,9 +359,18 @@ export class SettingsEditor2 extends BaseEditor { } } - const firstFocusable = this.settingsTree.getHTMLElement().querySelector(AbstractSettingRenderer.CONTROL_SELECTOR); - if (firstFocusable) { - (firstFocusable).focus(); + const focused = this.settingsTree.getFocus(); + if (!focused.length) { + this.settingsTree.focusFirst(); + } + + this.settingsTree.domFocus(); + + if (focusSettingInput) { + const controlInFocusedRow = this.settingsTree.getHTMLElement().querySelector(`.focused ${AbstractSettingRenderer.CONTROL_SELECTOR}`); + if (controlInFocusedRow) { + (controlInFocusedRow).focus(); + } } } @@ -509,6 +520,11 @@ export class SettingsEditor2 extends BaseEditor { this.settingsTree.reveal(elements[0], sourceTop); + // We need to shift focus from the setting that contains the link to the setting that's + // linked. Clicking on the link sets focus on the setting that contains the link, + // which is why we need the setTimeout + setTimeout(() => this.settingsTree.setFocus([elements[0]]), 50); + const domElements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), evt.targetKey); if (domElements && domElements[0]) { const control = domElements[0].querySelector(AbstractSettingRenderer.CONTROL_SELECTOR); @@ -569,48 +585,7 @@ export class SettingsEditor2 extends BaseEditor { })); this.createTOC(bodyContainer); - - this.createFocusSink( - bodyContainer, - e => { - if (DOM.findParentWithClass(e.relatedTarget, 'settings-editor-tree')) { - if (this.settingsTree.scrollTop > 0) { - const firstElement = this.settingsTree.firstVisibleElement; - - if (typeof firstElement !== 'undefined') { - this.settingsTree.reveal(firstElement, 0.1); - } - - return true; - } - } else { - const firstControl = this.settingsTree.getHTMLElement().querySelector(AbstractSettingRenderer.CONTROL_SELECTOR); - if (firstControl) { - (firstControl).focus(); - } - } - - return false; - }, - 'settings list focus helper'); - this.createSettingsTree(bodyContainer); - - this.createFocusSink( - bodyContainer, - e => { - if (DOM.findParentWithClass(e.relatedTarget, 'settings-editor-tree')) { - if (this.settingsTree.scrollTop < this.settingsTree.scrollHeight) { - const lastElement = this.settingsTree.lastVisibleElement; - this.settingsTree.reveal(lastElement, 0.9); - return true; - } - } - - return false; - }, - 'settings list focus helper' - ); } private addCtrlAInterceptor(container: HTMLElement): void { @@ -628,19 +603,6 @@ export class SettingsEditor2 extends BaseEditor { })); } - private createFocusSink(container: HTMLElement, callback: (e: any) => boolean, label: string): HTMLElement { - const listFocusSink = DOM.append(container, $('.settings-tree-focus-sink')); - listFocusSink.setAttribute('aria-label', label); - listFocusSink.tabIndex = 0; - this._register(DOM.addDisposableListener(listFocusSink, 'focus', (e: any) => { - if (e.relatedTarget && callback(e)) { - e.relatedTarget.focus(); - } - })); - - return listFocusSink; - } - private createTOC(parent: HTMLElement): void { this.tocTreeModel = this.instantiationService.createInstance(TOCTreeModel, this.viewState); this.tocTreeContainer = DOM.append(parent, $('.settings-toc-container')); @@ -668,6 +630,7 @@ export class SettingsEditor2 extends BaseEditor { } } else if (element && (!e.browserEvent || !(e.browserEvent).fromScroll)) { this.settingsTree.reveal(element, 0); + this.settingsTree.setFocus([element]); } })); @@ -717,7 +680,6 @@ export class SettingsEditor2 extends BaseEditor { this.settingsTreeContainer, this.viewState, this.settingRenderers.allRenderers)); - this.settingsTree.getHTMLElement().attributes.removeNamedItem('tabindex'); this._register(this.settingsTree.onDidScroll(() => { if (this.settingsTree.scrollTop === this.settingsTreeScrollTop) { @@ -725,6 +687,7 @@ export class SettingsEditor2 extends BaseEditor { } this.settingsTreeScrollTop = this.settingsTree.scrollTop; + updateSettingTreeTabOrder(this.settingsTreeContainer); // setTimeout because calling setChildren on the settingsTree can trigger onDidScroll, so it fires when // setChildren has called on the settings tree but not the toc tree yet, so their rendered elements are out of sync @@ -732,6 +695,20 @@ export class SettingsEditor2 extends BaseEditor { this.updateTreeScrollSync(); }, 0); })); + + // There is no different select state in the settings tree + this._register(this.settingsTree.onDidChangeFocus(e => { + const element = e.elements[0]; + if (this.treeFocusedElement === element) { + return; + } + + this.treeFocusedElement = element; + this.settingsTree.setSelection(element ? [element] : []); + + // Wait for rendering to complete + setTimeout(() => updateSettingTreeTabOrder(this.settingsTreeContainer), 0); + })); } private notifyNoSaveNeeded() { @@ -966,7 +943,7 @@ export class SettingsEditor2 extends BaseEditor { } const groups = this.defaultSettingsEditorModel.settingsGroups.slice(1); // Without commonlyUsed - const dividedGroups = collections.groupBy(groups, g => g.contributedByExtension ? 'extension' : 'core'); + const dividedGroups = collections.groupBy(groups, g => g.extensionInfo ? 'extension' : 'core'); const settingsResult = resolveSettingsTree(tocData, dividedGroups.core); const resolvedSettingsRoot = settingsResult.tree; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index c30a7ee401b..df568849718 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -5,7 +5,6 @@ import { BrowserFeatures } from 'vs/base/browser/canIUse'; import * as DOM from 'vs/base/browser/dom'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { alert as ariaAlert } from 'vs/base/browser/ui/aria/aria'; @@ -16,7 +15,7 @@ import { CachedListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { DefaultStyleController } from 'vs/base/browser/ui/list/listWidget'; import { ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IObjectTreeOptions, ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; +import { IObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree'; import { ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; import { ITreeFilter, ITreeModel, ITreeNode, ITreeRenderer, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { Action, IAction, Separator } from 'vs/base/common/actions'; @@ -43,7 +42,7 @@ import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticip import { getIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; import { ITOCEntry } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; import { ISettingsEditorViewState, settingKeyToDisplayFormat, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; -import { ExcludeSettingWidget, ISettingListChangeEvent, IListDataItem, ListSettingWidget, settingsHeaderForeground, settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground, ObjectSettingWidget, IObjectDataItem, IObjectEnumOption, ObjectValue, IObjectValueSuggester, IObjectKeySuggester } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; +import { ExcludeSettingWidget, ISettingListChangeEvent, IListDataItem, ListSettingWidget, settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground, ObjectSettingWidget, IObjectDataItem, IObjectEnumOption, ObjectValue, IObjectValueSuggester, IObjectKeySuggester, focusedRowBackground, focusedRowBorder, settingsHeaderForeground, rowHoverBackground } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; import { SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ISetting, ISettingsGroup, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; @@ -53,6 +52,9 @@ import { Codicon } from 'vs/base/common/codicons'; import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { IList } from 'vs/base/browser/ui/tree/indexTreeModel'; +import { IListService, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; const $ = DOM.$; @@ -451,6 +453,45 @@ export interface ISettingOverrideClickEvent { targetKey: string; } +function removeChildrenFromTabOrder(node: Element): void { + const focusableElements = node.querySelectorAll(` + [tabindex="0"], + input:not([tabindex="-1"]), + select:not([tabindex="-1"]), + textarea:not([tabindex="-1"]), + a:not([tabindex="-1"]), + button:not([tabindex="-1"]), + area:not([tabindex="-1"]) + `); + + focusableElements.forEach(element => { + element.setAttribute(AbstractSettingRenderer.ELEMENT_FOCUSABLE_ATTR, 'true'); + element.setAttribute('tabindex', '-1'); + }); +} + +function addChildrenToTabOrder(node: Element): void { + const focusableElements = node.querySelectorAll( + `[${AbstractSettingRenderer.ELEMENT_FOCUSABLE_ATTR}="true"]` + ); + + focusableElements.forEach(element => { + element.removeAttribute(AbstractSettingRenderer.ELEMENT_FOCUSABLE_ATTR); + element.setAttribute('tabindex', '0'); + }); +} + +export function updateSettingTreeTabOrder(container: Element): void { + const allRows = [...container.querySelectorAll(AbstractSettingRenderer.ALL_ROWS_SELECTOR)]; + const focusedRow = allRows.find(row => row.classList.contains('focused')); + + allRows.forEach(removeChildrenFromTabOrder); + + if (isDefined(focusedRow)) { + addChildrenToTabOrder(focusedRow); + } +} + export abstract class AbstractSettingRenderer extends Disposable implements ITreeRenderer { /** To override */ abstract get templateId(): string; @@ -459,9 +500,11 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre static readonly CONTROL_SELECTOR = '.' + AbstractSettingRenderer.CONTROL_CLASS; static readonly CONTENTS_CLASS = 'setting-item-contents'; static readonly CONTENTS_SELECTOR = '.' + AbstractSettingRenderer.CONTENTS_CLASS; + static readonly ALL_ROWS_SELECTOR = '.monaco-list-row'; static readonly SETTING_KEY_ATTR = 'data-key'; static readonly SETTING_ID_ATTR = 'data-id'; + static readonly ELEMENT_FOCUSABLE_ATTR = 'data-focusable'; private readonly _onDidClickOverrideElement = this._register(new Emitter()); readonly onDidClickOverrideElement: Event = this._onDidClickOverrideElement.event; @@ -598,7 +641,8 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre } const toolbar = new ToolBar(container, this._contextMenuService, { - toggleMenuTitle + toggleMenuTitle, + renderDropdownAsChildElement: true }); return toolbar; } @@ -606,7 +650,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre private fixToolbarIcon(toolbar: ToolBar): void { const button = toolbar.getElement().querySelector('.codicon-toolbar-more'); if (button) { - (button).tabIndex = -1; + (button).tabIndex = 0; // change icon from ellipsis to gear (button).classList.add('codicon-gear'); @@ -636,7 +680,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre template.labelElement.textContent = element.displayLabel; template.labelElement.title = titleTooltip; - template.descriptionElement.innerHTML = ''; + template.descriptionElement.innerText = ''; if (element.setting.descriptionIsMarkdown) { const disposables = new DisposableStore(); template.toDispose.add(disposables); @@ -649,7 +693,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const baseId = (element.displayCategory + '_' + element.displayLabel).replace(/ /g, '_').toLowerCase(); template.descriptionElement.id = baseId + '_setting_description'; - template.otherOverridesElement.innerHTML = ''; + template.otherOverridesElement.innerText = ''; template.otherOverridesElement.style.display = 'none'; if (element.overriddenScopeList.length) { template.otherOverridesElement.style.display = 'inline'; @@ -686,7 +730,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre if (deprecationText && element.setting.deprecationMessageIsMarkdown) { const disposables = new DisposableStore(); template.elementDisposables.add(disposables); - template.deprecationWarningElement.innerHTML = ''; + template.deprecationWarningElement.innerText = ''; template.deprecationWarningElement.appendChild(this.renderSettingMarkdown(element, element.setting.deprecationMessage!, template.elementDisposables)); } else { template.deprecationWarningElement.innerText = deprecationText; @@ -820,7 +864,7 @@ export class SettingGroupRenderer implements ITreeRenderer, index: number, templateData: IGroupTitleTemplate): void { - templateData.parent.innerHTML = ''; + templateData.parent.innerText = ''; const labelElement = DOM.append(templateData.parent, $('div.settings-group-title-label')); labelElement.classList.add(`settings-group-level-${element.element.level}`); labelElement.textContent = element.element.label; @@ -1247,6 +1291,15 @@ export class SettingTextRenderer extends AbstractSettingRenderer implements ITre })); common.toDispose.add(inputBox); inputBox.inputElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); + inputBox.inputElement.tabIndex = 0; + + // TODO@9at8: listWidget filters out all key events from input boxes, so we need to come up with a better way + // Disable ArrowUp and ArrowDown behaviour in favor of list navigation + common.toDispose.add(DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, e => { + if (e.equals(KeyCode.UpArrow) || e.equals(KeyCode.DownArrow)) { + e.preventDefault(); + } + })); const template: ISettingTextItemTemplate = { ...common, @@ -1299,6 +1352,7 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre const selectElement = common.controlElement.querySelector('select'); if (selectElement) { selectElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); + selectElement.tabIndex = 0; } common.toDispose.add( @@ -1365,7 +1419,7 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre template.selectBox.select(idx); template.onChange = idx => onChange(dataElement.setting.enum![idx]); - template.enumDescriptionElement.innerHTML = ''; + template.enumDescriptionElement.innerText = ''; } } @@ -1391,6 +1445,7 @@ export class SettingNumberRenderer extends AbstractSettingRenderer implements IT })); common.toDispose.add(inputBox); inputBox.inputElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); + inputBox.inputElement.tabIndex = 0; const template: ISettingNumberItemTemplate = { ...common, @@ -1503,13 +1558,6 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre // Prevent clicks from being handled by list toDispose.add(DOM.addDisposableListener(controlElement, 'mousedown', (e: IMouseEvent) => e.stopPropagation())); - - toDispose.add(DOM.addStandardDisposableListener(controlElement, 'keydown', (e: StandardKeyboardEvent) => { - if (e.keyCode === KeyCode.Escape) { - e.browserEvent.stopPropagation(); - } - })); - toDispose.add(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_ENTER, e => container.classList.add('mouseover'))); toDispose.add(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_LEAVE, e => container.classList.remove('mouseover'))); @@ -1833,11 +1881,7 @@ class SettingsTreeDelegate extends CachedListVirtualDelegate extends ObjectTreeModel { } } -export class SettingsTree extends ObjectTree { +export class SettingsTree extends WorkbenchObjectTree { constructor( container: HTMLElement, viewState: ISettingsEditorViewState, renderers: ITreeRenderer[], + @IContextKeyService contextKeyService: IContextKeyService, + @IListService listService: IListService, @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, + @IKeybindingService keybindingService: IKeybindingService, + @IAccessibilityService accessibilityService: IAccessibilityService, @IInstantiationService instantiationService: IInstantiationService, ) { super('SettingsTree', container, new SettingsTreeDelegate(), renderers, { + horizontalScrolling: false, supportDynamicHeights: true, identityProvider: { getId(e) { @@ -1874,9 +1923,6 @@ export class SettingsTree extends ObjectTree { } }, accessibilityProvider: { - getWidgetRole() { - return 'form'; - }, getAriaLabel() { // TODO@roblourens https://github.com/microsoft/vscode/issues/95862 return ''; @@ -1888,9 +1934,16 @@ export class SettingsTree extends ObjectTree { styleController: id => new DefaultStyleController(DOM.createStyleSheet(container), id), filter: instantiationService.createInstance(SettingsTreeFilter, viewState), smoothScrolling: configurationService.getValue('workbench.list.smoothScrolling'), - }); + multipleSelectionSupport: false, + }, + contextKeyService, + listService, + themeService, + configurationService, + keybindingService, + accessibilityService, + ); - this.disposables.clear(); this.disposables.add(registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { const activeBorderColor = theme.getColor(focusBorder); if (activeBorderColor) { @@ -1929,6 +1982,26 @@ export class SettingsTree extends ObjectTree { collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.invalid-input .setting-item-control .monaco-inputbox.idle { outline-width: 0; border-style:solid; border-width: 1px; border-color: ${invalidInputBorder}; }`); } + const focusedRowBackgroundColor = theme.getColor(focusedRowBackground); + if (focusedRowBackgroundColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list-row.focused .setting-item-contents, + .settings-editor > .settings-body > .settings-tree-container .monaco-list-row.focused .settings-group-title-label { background-color: ${focusedRowBackgroundColor}; }`); + } + + const rowHoverBackgroundColor = theme.getColor(rowHoverBackground); + if (rowHoverBackgroundColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list-row .setting-item-contents:hover, + .settings-editor > .settings-body > .settings-tree-container .monaco-list-row .settings-group-title-label:hover { background-color: ${rowHoverBackgroundColor}; }`); + } + + const focusedRowBorderColor = theme.getColor(focusedRowBorder); + if (focusedRowBorderColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .setting-item-contents::before, + .settings-editor > .settings-body > .settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .setting-item-contents::after { border-top: 1px solid ${focusedRowBorderColor} }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .settings-group-title-label::before, + .settings-editor > .settings-body > .settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .settings-group-title-label::after { border-top: 1px solid ${focusedRowBorderColor} }`); + } + const headerForegroundColor = theme.getColor(settingsHeaderForeground); if (headerForegroundColor) { collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label { color: ${headerForegroundColor}; }`); @@ -1939,6 +2012,12 @@ export class SettingsTree extends ObjectTree { if (focusBorderColor) { collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-markdown a:focus { outline-color: ${focusBorderColor} }`); } + + // const listActiveSelectionBackgroundColor = theme.getColor(listActiveSelectionBackground); + // if (listActiveSelectionBackgroundColor) { + // collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list-row.selected .setting-item-contents .setting-item-title { background-color: ${listActiveSelectionBackgroundColor}; }`); + // collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list-row.selected .settings-group-title-label { background-color: ${listActiveSelectionBackgroundColor}; }`); + // } })); this.getHTMLElement().classList.add('settings-editor-tree'); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index d7f85b56922..5eaab66e56c 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -16,7 +16,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import 'vs/css!./media/settingsWidgets'; import { localize } from 'vs/nls'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { foreground, inputBackground, inputBorder, inputForeground, listActiveSelectionBackground, listActiveSelectionForeground, listHoverBackground, listHoverForeground, listInactiveSelectionBackground, listInactiveSelectionForeground, registerColor, selectBackground, selectBorder, selectForeground, textLinkForeground, textPreformatForeground, editorWidgetBorder, textLinkActiveForeground, simpleCheckboxBackground, simpleCheckboxForeground, simpleCheckboxBorder } from 'vs/platform/theme/common/colorRegistry'; +import { foreground, inputBorder, inputForeground, listActiveSelectionBackground, listActiveSelectionForeground, listHoverBackground, listHoverForeground, listInactiveSelectionBackground, listInactiveSelectionForeground, registerColor, selectBackground, selectBorder, selectForeground, textLinkForeground, textPreformatForeground, editorWidgetBorder, textLinkActiveForeground, simpleCheckboxBackground, simpleCheckboxForeground, simpleCheckboxBorder, listFocusBackground, transparent, focusBorder } from 'vs/platform/theme/common/colorRegistry'; import { attachButtonStyler, attachInputBoxStyler, attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { disposableTimeout } from 'vs/base/common/async'; @@ -25,6 +25,7 @@ import { preferencesEditIcon } from 'vs/workbench/contrib/preferences/browser/pr import { SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { isIOS } from 'vs/base/common/platform'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; +import { PANEL_BORDER } from 'vs/workbench/common/theme'; const $ = DOM.$; export const settingsHeaderForeground = registerColor('settings.headerForeground', { light: '#444444', dark: '#e7e7e7', hc: '#ffffff' }, localize('headerForeground', "The foreground color for a section header or active title.")); @@ -46,15 +47,33 @@ export const settingsCheckboxForeground = registerColor('settings.checkboxForegr export const settingsCheckboxBorder = registerColor('settings.checkboxBorder', { dark: simpleCheckboxBorder, light: simpleCheckboxBorder, hc: simpleCheckboxBorder }, localize('settingsCheckboxBorder', "Settings editor checkbox border.")); // Text control colors -export const settingsTextInputBackground = registerColor('settings.textInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('textInputBoxBackground', "Settings editor text input box background.")); +export const settingsTextInputBackground = settingsSelectBackground; //registerColor('settings.textInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('textInputBoxBackground', "Settings editor text input box background.")); export const settingsTextInputForeground = registerColor('settings.textInputForeground', { dark: inputForeground, light: inputForeground, hc: inputForeground }, localize('textInputBoxForeground', "Settings editor text input box foreground.")); export const settingsTextInputBorder = registerColor('settings.textInputBorder', { dark: inputBorder, light: inputBorder, hc: inputBorder }, localize('textInputBoxBorder', "Settings editor text input box border.")); // Number control colors -export const settingsNumberInputBackground = registerColor('settings.numberInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('numberInputBoxBackground', "Settings editor number input box background.")); +export const settingsNumberInputBackground = settingsSelectBackground; // registerColor('settings.numberInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('numberInputBoxBackground', "Settings editor number input box background.")); export const settingsNumberInputForeground = registerColor('settings.numberInputForeground', { dark: inputForeground, light: inputForeground, hc: inputForeground }, localize('numberInputBoxForeground', "Settings editor number input box foreground.")); export const settingsNumberInputBorder = registerColor('settings.numberInputBorder', { dark: inputBorder, light: inputBorder, hc: inputBorder }, localize('numberInputBoxBorder', "Settings editor number input box border.")); +export const focusedRowBackground = registerColor('settings.focusedRowBackground', { + dark: transparent(PANEL_BORDER, .4), + light: transparent(listFocusBackground, .4), + hc: null +}, localize('focusedRowBackground', "The background color of a cell when the row is focused.")); + +export const rowHoverBackground = registerColor('notebook.rowHoverBackground', { + dark: transparent(focusedRowBackground, .5), + light: transparent(focusedRowBackground, .7), + hc: null +}, localize('notebook.rowHoverBackground', "The background color of a row when the row is hovered.")); + +export const focusedRowBorder = registerColor('notebook.focusedRowBorder', { + dark: Color.white.transparent(0.12), + light: Color.black.transparent(0.12), + hc: focusBorder +}, localize('notebook.focusedRowBorder', "The color of the row's top and bottom border when the row is focused.")); + registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { const checkboxBackgroundColor = theme.getColor(settingsCheckboxBackground); if (checkboxBackgroundColor) { @@ -527,7 +546,7 @@ export class ListSettingWidget extends AbstractListSettingWidget valueInput.element.classList.add('setting-list-valueInput'); this.listDisposables.add(attachInputBoxStyler(valueInput, this.themeService, { - inputBackground: settingsTextInputBackground, + inputBackground: settingsSelectBackground, inputForeground: settingsTextInputForeground, inputBorder: settingsTextInputBorder })); @@ -546,7 +565,7 @@ export class ListSettingWidget extends AbstractListSettingWidget siblingInput.element.classList.add('setting-list-siblingInput'); this.listDisposables.add(siblingInput); this.listDisposables.add(attachInputBoxStyler(siblingInput, this.themeService, { - inputBackground: settingsTextInputBackground, + inputBackground: settingsSelectBackground, inputForeground: settingsTextInputForeground, inputBorder: settingsTextInputBorder })); @@ -908,7 +927,7 @@ export class ObjectSettingWidget extends AbstractListSettingWidget { +export class TOCTree extends WorkbenchObjectTree { constructor( container: HTMLElement, viewState: ISettingsEditorViewState, + @IContextKeyService contextKeyService: IContextKeyService, + @IListService listService: IListService, @IThemeService themeService: IThemeService, - @IInstantiationService instantiationService: IInstantiationService + @IConfigurationService configurationService: IConfigurationService, + @IKeybindingService keybindingService: IKeybindingService, + @IAccessibilityService accessibilityService: IAccessibilityService, + @IInstantiationService instantiationService: IInstantiationService, ) { // test open mode const filter = instantiationService.createInstance(SettingsTreeFilter, viewState); - const options: IObjectTreeOptions = { + const options: IWorkbenchObjectTreeOptions = { filter, multipleSelectionSupport: false, identityProvider: { @@ -207,13 +216,23 @@ export class TOCTree extends ObjectTree { }, styleController: id => new DefaultStyleController(DOM.createStyleSheet(container), id), accessibilityProvider: instantiationService.createInstance(SettingsAccessibilityProvider), - collapseByDefault: true + collapseByDefault: true, + horizontalScrolling: false }; - super('SettingsTOC', container, + super( + 'SettingsTOC', + container, new TOCTreeDelegate(), [new TOCRenderer()], - options); + options, + contextKeyService, + listService, + themeService, + configurationService, + keybindingService, + accessibilityService, + ); this.disposables.add(attachStyler(themeService, { listBackground: editorBackground, diff --git a/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts b/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts index 9e2ae84e031..895c3df7b10 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts @@ -95,10 +95,10 @@ MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { MenuRegistry.appendMenuItem(MenuId.EditorContext, { group: 'z_commands', + when: EditorContextKeys.editorSimpleInput.toNegated(), command: { id: ShowAllCommandsAction.ID, title: localize('commandPalette', "Command Palette..."), - precondition: EditorContextKeys.editorSimpleInput.toNegated() }, order: 1 }); diff --git a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts index 7886563a6ad..70e0e6a2137 100644 --- a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts +++ b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts @@ -23,7 +23,6 @@ import { IProductService } from 'vs/platform/product/common/productService'; interface IConfiguration extends IWindowsConfiguration { update: { mode: string; }; - telemetry: { enableCrashReporter: boolean }; debug: { console: { wordWrap: boolean } }; editor: { accessibilitySupport: 'on' | 'off' | 'auto' }; } @@ -35,7 +34,6 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo private nativeFullScreen: boolean | undefined; private clickThroughInactive: boolean | undefined; private updateMode: string | undefined; - private enableCrashReporter: boolean | undefined; private debugConsoleWordWrap: boolean | undefined; private accessibilitySupport: 'on' | 'off' | 'auto' | undefined; @@ -92,12 +90,6 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo changed = true; } - // Crash reporter - if (typeof config.telemetry?.enableCrashReporter === 'boolean' && config.telemetry.enableCrashReporter !== this.enableCrashReporter) { - this.enableCrashReporter = config.telemetry.enableCrashReporter; - changed = true; - } - // On linux turning on accessibility support will also pass this flag to the chrome renderer, thus a restart is required if (isLinux && typeof config.editor?.accessibilitySupport === 'string' && config.editor.accessibilitySupport !== this.accessibilitySupport) { this.accessibilitySupport = config.editor.accessibilitySupport; diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index 6f0a32f639a..0446e6aa795 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -53,9 +53,10 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Event } from 'vs/base/common/event'; import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { RemoteWindowActiveIndicator } from 'vs/workbench/contrib/remote/browser/remoteIndicator'; +import { RemoteStatusIndicator } from 'vs/workbench/contrib/remote/browser/remoteIndicator'; import { inQuickPickContextKeyValue } from 'vs/workbench/browser/quickaccess'; import { Codicon, registerIcon } from 'vs/base/common/codicons'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; export interface HelpInformation { extensionDescription: IExtensionDescription; @@ -481,6 +482,7 @@ export class RemoteViewPaneContainer extends FilterViewPaneContainer implements @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @ITerminalService private readonly terminalService: ITerminalService ) { super(VIEWLET_ID, remoteExplorerService.onDidChangeTargetType, configurationService, layoutService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService, viewDescriptorService); this.addConstantViewDescriptors([this.helpPanelDescriptor]); @@ -555,7 +557,7 @@ export class RemoteViewPaneContainer extends FilterViewPaneContainer implements // This context key is set to false in the constructor, but is expected to be changed by resolver extensions to enable the forwarded ports view. const viewEnabled: boolean = !!forwardedPortsViewEnabled.getValue(this.contextKeyService); if (this.environmentService.configuration.remoteAuthority && !this.tunnelPanelDescriptor && viewEnabled) { - this.tunnelPanelDescriptor = new TunnelPanelDescriptor(new TunnelViewModel(this.remoteExplorerService), this.environmentService); + this.tunnelPanelDescriptor = new TunnelPanelDescriptor(new TunnelViewModel(this.remoteExplorerService, this.terminalService), this.environmentService); const viewsRegistry = Registry.as(Extensions.ViewsRegistry); viewsRegistry.registerViews([this.tunnelPanelDescriptor!], this.viewContainer); } @@ -836,4 +838,4 @@ class RemoteAgentConnectionStatusListener implements IWorkbenchContribution { const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteAgentConnectionStatusListener, LifecyclePhase.Eventually); -workbenchContributionsRegistry.registerWorkbenchContribution(RemoteWindowActiveIndicator, LifecyclePhase.Starting); +workbenchContributionsRegistry.registerWorkbenchContribution(RemoteStatusIndicator, LifecyclePhase.Starting); diff --git a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts index 7aec94c4f7e..fb51919e9b1 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { STATUS_BAR_HOST_NAME_BACKGROUND, STATUS_BAR_HOST_NAME_FOREGROUND } from 'vs/workbench/common/theme'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, dispose } from 'vs/base/common/lifecycle'; import { MenuId, IMenuService, MenuItemAction, IMenu, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; @@ -21,88 +21,112 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { RemoteConnectionState, Deprecated_RemoteAuthorityContext } from 'vs/workbench/browser/contextkeys'; +import { RemoteConnectionState } from 'vs/workbench/browser/contextkeys'; import { isWeb } from 'vs/base/common/platform'; import { once } from 'vs/base/common/functional'; -const WINDOW_ACTIONS_COMMAND_ID = 'workbench.action.remote.showMenu'; -const CLOSE_REMOTE_COMMAND_ID = 'workbench.action.remote.close'; -const SHOW_CLOSE_REMOTE_COMMAND_ID = !isWeb; // web does not have a "Close Remote" command +export class RemoteStatusIndicator extends Disposable implements IWorkbenchContribution { -export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenchContribution { + private static REMOTE_ACTIONS_COMMAND_ID = 'workbench.action.remote.showMenu'; + private static CLOSE_REMOTE_COMMAND_ID = 'workbench.action.remote.close'; + private static SHOW_CLOSE_REMOTE_COMMAND_ID = !isWeb; // web does not have a "Close Remote" command - private windowIndicatorEntry: IStatusbarEntryAccessor | undefined; - private windowCommandMenu: IMenu; - private hasWindowActions: boolean = false; - private remoteAuthority: string | undefined; + private remoteStatusEntry: IStatusbarEntryAccessor | undefined; + + private remoteMenu = this._register(this.menuService.createMenu(MenuId.StatusBarWindowIndicatorMenu, this.contextKeyService)); + private hasRemoteActions = false; + + private remoteAuthority = this.environmentService.configuration.remoteAuthority; private connectionState: 'initializing' | 'connected' | 'disconnected' | undefined = undefined; + private connectionStateContextKey = RemoteConnectionState.bindTo(this.contextKeyService); constructor( @IStatusbarService private readonly statusbarService: IStatusbarService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @ILabelService private readonly labelService: ILabelService, @IContextKeyService private contextKeyService: IContextKeyService, @IMenuService private menuService: IMenuService, @IQuickInputService private readonly quickInputService: IQuickInputService, @ICommandService private readonly commandService: ICommandService, - @IExtensionService extensionService: IExtensionService, - @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, - @IHostService hostService: IHostService + @IExtensionService private readonly extensionService: IExtensionService, + @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, + @IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService, + @IHostService private readonly hostService: IHostService ) { super(); - this.windowCommandMenu = this.menuService.createMenu(MenuId.StatusBarWindowIndicatorMenu, this.contextKeyService); - this._register(this.windowCommandMenu); + // Set initial connection state + if (this.remoteAuthority) { + this.connectionState = 'initializing'; + this.connectionStateContextKey.set(this.connectionState); + } + this.registerActions(); + this.registerListeners(); + + this.updateWhenInstalledExtensionsRegistered(); + this.updateRemoteStatusIndicator(); + } + + private registerActions(): void { const category = { value: nls.localize('remote.category', "Remote"), original: 'Remote' }; + + // Show Remote Menu const that = this; registerAction2(class extends Action2 { constructor() { super({ - id: WINDOW_ACTIONS_COMMAND_ID, + id: RemoteStatusIndicator.REMOTE_ACTIONS_COMMAND_ID, category, title: { value: nls.localize('remote.showMenu', "Show Remote Menu"), original: 'Show Remote Menu' }, f1: true, }); } - run = () => that.showIndicatorActions(that.windowCommandMenu); + run = () => that.showRemoteMenu(that.remoteMenu); }); - this.remoteAuthority = environmentService.configuration.remoteAuthority; - Deprecated_RemoteAuthorityContext.bindTo(this.contextKeyService).set(this.remoteAuthority || ''); + // Close Remote Connection + if (RemoteStatusIndicator.SHOW_CLOSE_REMOTE_COMMAND_ID && this.remoteAuthority) { + registerAction2(class extends Action2 { + constructor() { + super({ + id: RemoteStatusIndicator.CLOSE_REMOTE_COMMAND_ID, + category, + title: { value: nls.localize('remote.close', "Close Remote Connection"), original: 'Close Remote Connection' }, + f1: true + }); + } + run = () => that.remoteAuthority && that.hostService.openWindow({ forceReuseWindow: true }); + }); + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '6_close', + command: { + id: RemoteStatusIndicator.CLOSE_REMOTE_COMMAND_ID, + title: nls.localize({ key: 'miCloseRemote', comment: ['&& denotes a mnemonic'] }, "Close Re&&mote Connection") + }, + order: 3.5 + }); + } + } + + private registerListeners(): void { + + // Menu changes + this._register(this.remoteMenu.onDidChange(() => this.updateRemoteActions())); + + // Update indicator when formatter changes as it may have an impact on the remote label + this._register(this.labelService.onDidChangeFormatters(() => this.updateRemoteStatusIndicator())); + + // Update based on remote indicator changes if any + const remoteIndicator = this.environmentService.options?.windowIndicator; + if (remoteIndicator) { + this._register(remoteIndicator.onDidChange(() => this.updateRemoteStatusIndicator())); + } + + // Listen to changes of the connection if (this.remoteAuthority) { - - if (SHOW_CLOSE_REMOTE_COMMAND_ID) { - registerAction2(class extends Action2 { - constructor() { - super({ - id: CLOSE_REMOTE_COMMAND_ID, - category, - title: { value: nls.localize('remote.close', "Close Remote Connection"), original: 'Close Remote Connection' }, - f1: true - }); - } - run = () => that.remoteAuthority && hostService.openWindow({ forceReuseWindow: true }); - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '6_close', - command: { - id: CLOSE_REMOTE_COMMAND_ID, - title: nls.localize({ key: 'miCloseRemote', comment: ['&& denotes a mnemonic'] }, "Close Re&&mote Connection") - }, - order: 3.5 - }); - } - - // Pending entry until extensions are ready - this.renderWindowIndicator('$(sync~spin) ' + nls.localize('host.open', "Opening Remote..."), undefined, WINDOW_ACTIONS_COMMAND_ID); - this.connectionState = 'initializing'; - RemoteConnectionState.bindTo(this.contextKeyService).set(this.connectionState); - - const connection = remoteAgentService.getConnection(); + const connection = this.remoteAgentService.getConnection(); if (connection) { this._register(connection.onDidStateChange((e) => { switch (e.type) { @@ -119,72 +143,106 @@ export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenc })); } } + } - extensionService.whenInstalledExtensionsRegistered().then(_ => { - if (this.remoteAuthority) { - this._register(this.labelService.onDidChangeFormatters(e => this.updateWindowIndicator())); - remoteAuthorityResolverService.resolveAuthority(this.remoteAuthority).then(() => this.setDisconnected(false), () => this.setDisconnected(true)); - } - this._register(this.windowCommandMenu.onDidChange(e => this.updateWindowActions())); - this.updateWindowIndicator(); - }); + private async updateWhenInstalledExtensionsRegistered(): Promise { + await this.extensionService.whenInstalledExtensionsRegistered(); + + const remoteAuthority = this.remoteAuthority; + if (remoteAuthority) { + + // Try to resolve the authority to figure out connection state + (async () => { + try { + await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority); + + this.setDisconnected(false); + } catch (error) { + this.setDisconnected(true); + } + })(); + } + + this.updateRemoteStatusIndicator(); } private setDisconnected(isDisconnected: boolean): void { const newState = isDisconnected ? 'disconnected' : 'connected'; if (this.connectionState !== newState) { this.connectionState = newState; - RemoteConnectionState.bindTo(this.contextKeyService).set(this.connectionState); - Deprecated_RemoteAuthorityContext.bindTo(this.contextKeyService).set(isDisconnected ? `disconnected/${this.remoteAuthority!}` : this.remoteAuthority!); - this.updateWindowIndicator(); + this.connectionStateContextKey.set(this.connectionState); + + this.updateRemoteStatusIndicator(); } } - private updateWindowIndicator(): void { - const windowActionCommand = (this.remoteAuthority || this.windowCommandMenu.getActions().length) ? WINDOW_ACTIONS_COMMAND_ID : undefined; - if (this.remoteAuthority) { + private updateRemoteActions() { + const newHasWindowActions = this.remoteMenu.getActions().length > 0; + if (newHasWindowActions !== this.hasRemoteActions) { + this.hasRemoteActions = newHasWindowActions; + + this.updateRemoteStatusIndicator(); + } + } + + private updateRemoteStatusIndicator(): void { + + // Remote indicator: show if provided via options + const remoteIndicator = this.environmentService.options?.windowIndicator; + if (remoteIndicator) { + this.renderRemoteStatusIndicator(remoteIndicator.label, remoteIndicator.tooltip, remoteIndicator.command); + } + + // Remote Authority: show connection state + else if (this.remoteAuthority) { const hostLabel = this.labelService.getHostLabel(REMOTE_HOST_SCHEME, this.remoteAuthority) || this.remoteAuthority; - if (this.connectionState !== 'disconnected') { - this.renderWindowIndicator(`$(remote) ${hostLabel}`, nls.localize('host.tooltip', "Editing on {0}", hostLabel), windowActionCommand); - } else { - this.renderWindowIndicator(`$(alert) ${nls.localize('disconnectedFrom', "Disconnected from")} ${hostLabel}`, nls.localize('host.tooltipDisconnected', "Disconnected from {0}", hostLabel), windowActionCommand); - } - } else { - if (windowActionCommand) { - this.renderWindowIndicator(`$(remote)`, nls.localize('noHost.tooltip', "Open a remote window"), windowActionCommand); - } else if (this.windowIndicatorEntry) { - this.windowIndicatorEntry.dispose(); - this.windowIndicatorEntry = undefined; + switch (this.connectionState) { + case 'initializing': + this.renderRemoteStatusIndicator(`$(sync~spin) ${nls.localize('host.open', "Opening Remote...")}`, nls.localize('host.open', "Opening Remote...")); + break; + case 'disconnected': + this.renderRemoteStatusIndicator(`$(alert) ${nls.localize('disconnectedFrom', "Disconnected from {0}", hostLabel)}`, nls.localize('host.tooltipDisconnected', "Disconnected from {0}", hostLabel)); + break; + default: + this.renderRemoteStatusIndicator(`$(remote) ${hostLabel}`, nls.localize('host.tooltip', "Editing on {0}", hostLabel)); } } + + // Remote Extensions Installed: offer the indicator to show actions + else if (this.remoteMenu.getActions().length > 0) { + this.renderRemoteStatusIndicator(`$(remote)`, nls.localize('noHost.tooltip', "Open a Remote Window")); + } + + // No Remote Extensions: hide status indicator + else { + dispose(this.remoteStatusEntry); + this.remoteStatusEntry = undefined; + } } - private updateWindowActions() { - const newHasWindowActions = this.windowCommandMenu.getActions().length > 0; - if (newHasWindowActions !== this.hasWindowActions) { - this.hasWindowActions = newHasWindowActions; - this.updateWindowIndicator(); + private renderRemoteStatusIndicator(text: string, tooltip?: string, command?: string): void { + const name = nls.localize('remoteHost', "Remote Host"); + if (typeof command !== 'string' && this.remoteMenu.getActions().length > 0) { + command = RemoteStatusIndicator.REMOTE_ACTIONS_COMMAND_ID; } - } - private renderWindowIndicator(text: string, tooltip?: string, command?: string): void { const properties: IStatusbarEntry = { backgroundColor: themeColorFromId(STATUS_BAR_HOST_NAME_BACKGROUND), color: themeColorFromId(STATUS_BAR_HOST_NAME_FOREGROUND), - ariaLabel: nls.localize('remote', "Remote"), + ariaLabel: name, text, tooltip, command }; - if (this.windowIndicatorEntry) { - this.windowIndicatorEntry.update(properties); + + if (this.remoteStatusEntry) { + this.remoteStatusEntry.update(properties); } else { - this.windowIndicatorEntry = this.statusbarService.addEntry(properties, 'status.host', nls.localize('status.host', "Remote Host"), StatusbarAlignment.LEFT, Number.MAX_VALUE /* first entry */); + this.remoteStatusEntry = this.statusbarService.addEntry(properties, 'status.host', name, StatusbarAlignment.LEFT, Number.MAX_VALUE /* first entry */); } } - private showIndicatorActions(menu: IMenu) { - + private showRemoteMenu(menu: IMenu) { const actions = menu.getActions(); const items: (IQuickPickItem | IQuickPickSeparator)[] = []; @@ -192,6 +250,7 @@ export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenc if (items.length) { items.push({ type: 'separator' }); } + for (let action of actionGroup[1]) { if (action instanceof MenuItemAction) { let label = typeof action.item.title === 'string' ? action.item.title : action.item.title.value; @@ -199,6 +258,7 @@ export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenc const category = typeof action.item.category === 'string' ? action.item.category : action.item.category.value; label = nls.localize('cat.title', "{0}: {1}", category, label); } + items.push({ type: 'item', id: action.item.id, @@ -208,13 +268,14 @@ export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenc } } - if (SHOW_CLOSE_REMOTE_COMMAND_ID && this.remoteAuthority) { + if (RemoteStatusIndicator.SHOW_CLOSE_REMOTE_COMMAND_ID && this.remoteAuthority) { if (items.length) { items.push({ type: 'separator' }); } + items.push({ type: 'item', - id: CLOSE_REMOTE_COMMAND_ID, + id: RemoteStatusIndicator.CLOSE_REMOTE_COMMAND_ID, label: nls.localize('closeRemote.title', 'Close Remote Connection') }); } @@ -227,8 +288,10 @@ export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenc if (selectedItems.length === 1) { this.commandService.executeCommand(selectedItems[0].id!); } + quickPick.hide(); })); + quickPick.show(); } } diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 930d4e799cc..3b398442e30 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -37,11 +37,13 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { URI } from 'vs/base/common/uri'; -import { RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { isLocalhost, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { UrlFinder } from 'vs/workbench/contrib/remote/browser/urlFinder'; export const forwardedPortsViewEnabled = new RawContextKey('forwardedPortsViewEnabled', false); @@ -72,7 +74,8 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { private _candidates: Map = new Map(); constructor( - @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService) { + @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, + @ITerminalService readonly terminalService: ITerminalService) { super(); this.model = remoteExplorerService.tunnelModel; this._register(this.model.onForwardPort(() => this._onForwardedPortsChanged.fire())); @@ -86,6 +89,11 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { remotePort: 0, description: '' }; + + const urlFinder = this._register(new UrlFinder(terminalService)); + this._register(urlFinder.onDidMatchLocalUrl(localUrl => { + this.model.forward(localUrl); + })); } async groups(): Promise { @@ -155,18 +163,34 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { }); } + private mapHasTunnel(map: Map, host: string, port: number): boolean { + if (!isLocalhost(host)) { + return map.has(MakeAddress(host, port)); + } + + const stringAddress = MakeAddress('localhost', port); + if (map.has(stringAddress)) { + return true; + } + const numberAddress = MakeAddress('127.0.0.1', port); + if (map.has(numberAddress)) { + return true; + } + return false; + } + get candidates(): TunnelItem[] { const candidates: TunnelItem[] = []; this._candidates.forEach(value => { - let key = MakeAddress(value.host, value.port); - if (!this.model.forwarded.has(key) && !this.model.detected.has(key)) { + if (!this.mapHasTunnel(this.model.forwarded, value.host, value.port) && + !this.mapHasTunnel(this.model.detected, value.host, value.port)) { // The host:port hasn't been forwarded or detected. However, if the candidate is 0.0.0.0, // also check that the port hasn't already been forwarded with localhost, and vice versa. // For example: no need to show 0.0.0.0:3000 as a candidate if localhost:3000 is already forwarded. const otherHost = value.host === '0.0.0.0' ? 'localhost' : (value.host === 'localhost' ? '0.0.0.0' : undefined); if (otherHost) { - key = MakeAddress(otherHost, value.port); - if (this.model.forwarded.has(key) || this.model.detected.has(key)) { + if (this.mapHasTunnel(this.model.forwarded, otherHost, value.port) || + this.mapHasTunnel(this.model.detected, otherHost, value.port)) { return; } } @@ -403,11 +427,11 @@ class TunnelItem implements ITunnelItem { get label(): string { if (this.name) { return nls.localize('remote.tunnelsView.forwardedPortLabel0', "{0}", this.name); - } else if (this.localAddress && (this.remoteHost !== 'localhost')) { + } else if (this.localAddress && !isLocalhost(this.remoteHost)) { return nls.localize('remote.tunnelsView.forwardedPortLabel2', "{0}:{1} \u2192 {2}", this.remoteHost, this.remotePort, this.localAddress); } else if (this.localAddress) { return nls.localize('remote.tunnelsView.forwardedPortLabel3', "{0} \u2192 {1}", this.remotePort, this.localAddress); - } else if (this.remoteHost !== 'localhost') { + } else if (!isLocalhost(this.remoteHost)) { return nls.localize('remote.tunnelsView.forwardedPortLabel4', "{0}:{1}", this.remoteHost, this.remotePort); } else { return nls.localize('remote.tunnelsView.forwardedPortLabel5', "{0}", this.remotePort); diff --git a/src/vs/workbench/contrib/remote/browser/urlFinder.ts b/src/vs/workbench/contrib/remote/browser/urlFinder.ts new file mode 100644 index 00000000000..d0ee06fd48b --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/urlFinder.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 { ITerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; + +export class UrlFinder extends Disposable { + private static readonly terminalCodesRegex = /(?:\u001B|\u009B)[\[\]()#;?]*(?:(?:(?:[a-zA-Z0-9]*(?:;[a-zA-Z0-9]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-PR-TZcf-ntqry=><~]))/g; + /** + * Local server url pattern matching following urls: + * http://localhost:3000/ - commonly used across multiple frameworks + * https://127.0.0.1:5001/ - ASP.NET + * http://:8080 - Beego Golang + * http://0.0.0.0:4000 - Elixir Phoenix + */ + private static readonly localUrlRegex = /\b\w{2,20}:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|:\d{2,5})[\w\-\.\~:\/\?\#[\]\@!\$&\(\)\*\+\,\;\=]*/gim; + + private _onDidMatchLocalUrl: Emitter<{ host: string, port: number }> = new Emitter(); + public readonly onDidMatchLocalUrl = this._onDidMatchLocalUrl.event; + private listeners: Map = new Map(); + + constructor(terminalService: ITerminalService) { + super(); + terminalService.terminalInstances.forEach(instance => { + this.listeners.set(instance, instance.onData(data => { + this.processData(data); + })); + }); + this._register(terminalService.onInstanceCreated(instance => { + this.listeners.set(instance, instance.onData(data => { + this.processData(data); + })); + })); + this._register(terminalService.onInstanceDisposed(instance => { + this.listeners.delete(instance); + })); + } + + dispose() { + super.dispose(); + const listeners = this.listeners.values(); + for (const listener of listeners) { + listener.dispose(); + } + } + + private processData(data: string) { + // strip ANSI terminal codes + data = data.replace(UrlFinder.terminalCodesRegex, ''); + const urlMatches = data.match(UrlFinder.localUrlRegex) || []; + urlMatches.forEach((match) => { + // check if valid url + const serverUrl = new URL(match); + if (serverUrl) { + // check if the port is a valid integer value + const port = parseFloat(serverUrl.port!); + if (!isNaN(port) && Number.isInteger(port) && port > 0 && port <= 65535) { + // normalize the host name + let host = serverUrl.hostname; + if (host !== '0.0.0.0' && host !== '127.0.0.1') { + host = 'localhost'; + } + this._onDidMatchLocalUrl.fire({ port, host }); + } + } + }); + } +} diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 450448112bb..14550a5bcb7 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -982,7 +982,7 @@ function compareChanges(a: IChange, b: IChange): number { return a.originalEndLineNumber - b.originalEndLineNumber; } -function createProviderComparer(uri: URI): (a: ISCMProvider, b: ISCMProvider) => number { +export function createProviderComparer(uri: URI): (a: ISCMProvider, b: ISCMProvider) => number { return (a, b) => { const aIsParent = isEqualOrParent(uri, a.rootUri!); const bIsParent = isEqualOrParent(uri, b.rootUri!); @@ -999,6 +999,22 @@ function createProviderComparer(uri: URI): (a: ISCMProvider, b: ISCMProvider) => }; } +export async function getOriginalResource(scmService: ISCMService, uri: URI): Promise { + const providers = scmService.repositories.map(r => r.provider); + const rootedProviders = providers.filter(p => !!p.rootUri); + + rootedProviders.sort(createProviderComparer(uri)); + + const result = await first(rootedProviders.map(p => () => p.getOriginalResource(uri))); + + if (result) { + return result; + } + + const nonRootedProviders = providers.filter(p => !p.rootUri); + return first(nonRootedProviders.map(p => () => p.getOriginalResource(uri))); +} + export class DirtyDiffModel extends Disposable { private _originalModel: IResolvedTextFileEditorModel | null = null; @@ -1155,19 +1171,7 @@ export class DirtyDiffModel extends Disposable { } const uri = this._model.resource; - const providers = this.scmService.repositories.map(r => r.provider); - const rootedProviders = providers.filter(p => !!p.rootUri); - - rootedProviders.sort(createProviderComparer(uri)); - - const result = await first(rootedProviders.map(p => () => p.getOriginalResource(uri))); - - if (result) { - return result; - } - - const nonRootedProviders = providers.filter(p => !p.rootUri); - return first(nonRootedProviders.map(p => () => p.getOriginalResource(uri))); + return getOriginalResource(this.scmService, uri); } findNextClosestChange(lineNumber: number, inclusive = true): number { diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 9947f240bf2..5fe8b5d58e8 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -232,3 +232,14 @@ .scm-view .scm-editor-container .mtk1 { color: inherit; } + +/* Repositories */ + +.scm-repositories-view .scm-provider { + margin: 0 12px 0 20px; + overflow: hidden; +} + +.scm-repositories-view .scm-provider > .label > .name { + font-weight: normal; +} diff --git a/src/vs/workbench/contrib/scm/browser/menus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts index 500f7fa585a..4fd31d3a776 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -10,9 +10,9 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; import { IAction } from 'vs/base/common/actions'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { ISCMResource, ISCMResourceGroup, ISCMProvider, ISCMRepository } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMResource, ISCMResourceGroup, ISCMProvider, ISCMRepository, ISCMService, ISCMMenus, ISCMRepositoryMenus } from 'vs/workbench/contrib/scm/common/scm'; import { equals } from 'vs/base/common/arrays'; -import { ISplice, ISequence } from 'vs/base/common/sequence'; +import { ISplice } from 'vs/base/common/sequence'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -20,7 +20,7 @@ function actionEquals(a: IAction, b: IAction): boolean { return a.id === b.id; } -export class SCMTitleMenu { +export class SCMTitleMenu implements IDisposable { private _actions: IAction[] = []; get actions(): IAction[] { return this._actions; } @@ -75,7 +75,7 @@ interface IContextualResourceMenuItem { dispose(): void; } -class SCMMenusItem { +class SCMMenusItem implements IDisposable { private _resourceGroupMenu: IMenu | undefined; get resourceGroupMenu(): IMenu { @@ -152,25 +152,35 @@ class SCMMenusItem { } } -export class SCMRepositoryMenus implements IDisposable { +export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { private contextKeyService: IContextKeyService; readonly titleMenu: SCMTitleMenu; - private repositoryMenu: IMenu | undefined; private readonly resourceGroups: ISCMResourceGroup[] = []; private readonly resourceGroupMenusItems = new Map(); + private _repositoryMenu: IMenu | undefined; + get repositoryMenu(): IMenu { + if (!this._repositoryMenu) { + this._repositoryMenu = this.menuService.createMenu(MenuId.SCMSourceControl, this.contextKeyService); + this.disposables.add(this._repositoryMenu); + } + + return this._repositoryMenu; + } + private readonly disposables = new DisposableStore(); constructor( - readonly provider: ISCMProvider, + provider: ISCMProvider, @IContextKeyService contextKeyService: IContextKeyService, @IInstantiationService instantiationService: IInstantiationService, @IMenuService private readonly menuService: IMenuService ) { this.contextKeyService = contextKeyService.createScoped(); this.contextKeyService.createKey('scmProvider', provider.contextValue); + this.contextKeyService.createKey('scmProviderRootUri', provider.rootUri?.toString()); this.contextKeyService.createKey('scmProviderHasRootUri', !!provider.rootUri); const serviceCollection = new ServiceCollection([IContextKeyService, this.contextKeyService]); @@ -181,15 +191,6 @@ export class SCMRepositoryMenus implements IDisposable { this.onDidSpliceGroups({ start: 0, deleteCount: 0, toInsert: provider.groups.elements }); } - getRepositoryMenu(): IMenu { - if (!this.repositoryMenu) { - this.repositoryMenu = this.menuService.createMenu(MenuId.SCMSourceControl, this.contextKeyService); - this.disposables.add(this.repositoryMenu); - } - - return this.repositoryMenu; - } - getResourceGroupMenu(group: ISCMResourceGroup): IMenu { return this.getOrCreateResourceGroupMenusItem(group).resourceGroupMenu; } @@ -233,48 +234,41 @@ export class SCMRepositoryMenus implements IDisposable { } } -export class SCMMenus { +export class SCMMenus implements ISCMMenus, IDisposable { readonly titleMenu: SCMTitleMenu; private readonly disposables = new DisposableStore(); - private readonly entries: { repository: ISCMRepository, dispose: () => void }[] = []; - private readonly menus = new Map(); + private readonly menus = new Map void }>(); constructor( - repositories: ISequence, + @ISCMService scmService: ISCMService, @IInstantiationService private instantiationService: IInstantiationService ) { this.titleMenu = instantiationService.createInstance(SCMTitleMenu); + scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); + } - repositories.onDidSplice(this.onDidSplice, this, this.disposables); - this.onDidSplice({ start: 0, deleteCount: 0, toInsert: repositories.elements }); + private onDidRemoveRepository(repository: ISCMRepository): void { + const menus = this.menus.get(repository.provider); + menus?.dispose(); + this.menus.delete(repository.provider); } getRepositoryMenus(provider: ISCMProvider): SCMRepositoryMenus { - if (!this.menus.has(provider)) { - throw new Error('SCM Repository menu not found'); - } + let result = this.menus.get(provider); - return this.menus.get(provider)!; - } - - private onDidSplice({ start, deleteCount, toInsert }: ISplice): void { - const entriesToInsert = toInsert.map(repository => { - const menus = this.instantiationService.createInstance(SCMRepositoryMenus, repository.provider); + if (!result) { + const menus = this.instantiationService.createInstance(SCMRepositoryMenus, provider); const dispose = () => { menus.dispose(); - this.menus.delete(repository.provider); + this.menus.delete(provider); }; - this.menus.set(repository.provider, menus); - return { repository, dispose }; - }); - - const deletedEntries = this.entries.splice(start, deleteCount, ...entriesToInsert); - - for (const entry of deletedEntries) { - entry.dispose(); + result = { menus, dispose }; + this.menus.set(provider, result); } + + return result.menus; } dispose(): void { diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index b0cda5d05ad..bf905250784 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { DirtyDiffWorkbenchController } from './dirtydiffDecorator'; -import { VIEWLET_ID, ISCMRepository, ISCMService, VIEW_PANE_ID, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm'; +import { VIEWLET_ID, ISCMRepository, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { SCMStatusController } from './activity'; @@ -24,6 +24,8 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; import { Codicon } from 'vs/base/common/codicons'; import { SCMViewPane } from 'vs/workbench/contrib/scm/browser/scmViewPane'; +import { SCMViewService } from 'vs/workbench/contrib/scm/browser/scmViewService'; +import { SCMRepositoriesViewPane } from 'vs/workbench/contrib/scm/browser/scmRepositoriesViewPane'; ModesRegistry.registerLanguage({ id: 'scminput', @@ -59,6 +61,23 @@ viewsRegistry.registerViews([{ canToggleVisibility: true, workspace: true, canMoveView: true, + weight: 80, + order: -999, + containerIcon: Codicon.sourceControl.classNames +}], viewContainer); + +viewsRegistry.registerViews([{ + id: REPOSITORIES_VIEW_PANE_ID, + name: localize('source control repositories', "Source Control Repositories"), + ctorDescriptor: new SyncDescriptor(SCMRepositoriesViewPane), + canToggleVisibility: true, + hideByDefault: true, + workspace: true, + canMoveView: true, + weight: 20, + order: -1000, + when: ContextKeyExpr.and(ContextKeyExpr.has('scm.providerCount'), ContextKeyExpr.notEquals('scm.providerCount', 0)), + // readonly when = ContextKeyExpr.or(ContextKeyExpr.equals('config.scm.alwaysShowProviders', true), ContextKeyExpr.and(ContextKeyExpr.notEquals('scm.providerCount', 0), ContextKeyExpr.notEquals('scm.providerCount', 1))); containerIcon: Codicon.sourceControl.classNames }], viewContainer); @@ -168,6 +187,11 @@ Registry.as(ConfigurationExtensions.Configuration).regis type: 'boolean', markdownDescription: localize('alwaysShowRepository', "Controls whether repositories should always be visible in the SCM view."), default: false + }, + 'scm.repositories.visible': { + type: 'number', + description: localize('providersVisible', "Controls how many repositories are visible in the Source Control Repositories section. Set to `0` to be able to manually resize the view."), + default: 10 } } }); @@ -225,3 +249,4 @@ MenuRegistry.appendMenuItem(MenuId.SCMSourceControl, { }); registerSingleton(ISCMService, SCMService); +registerSingleton(ISCMViewService, SCMViewService); diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts new file mode 100644 index 00000000000..503dd541418 --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts @@ -0,0 +1,203 @@ +/*--------------------------------------------------------------------------------------------- + * 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/scm'; +import { localize } from 'vs/nls'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { append, $ } from 'vs/base/browser/dom'; +import { IListVirtualDelegate, IListContextMenuEvent, IListEvent } from 'vs/base/browser/ui/list/list'; +import { ISCMRepository, ISCMService, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IAction, IActionViewItem } from 'vs/base/common/actions'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { WorkbenchList } from 'vs/platform/list/browser/listService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { RepositoryRenderer } from 'vs/workbench/contrib/scm/browser/scmRepositoryRenderer'; +import { collectContextMenuActions, StatusBarAction, StatusBarActionViewItem } from 'vs/workbench/contrib/scm/browser/util'; +import { Orientation } from 'vs/base/browser/ui/sash/sash'; + +class ListDelegate implements IListVirtualDelegate { + + getHeight(): number { + return 22; + } + + getTemplateId(): string { + return RepositoryRenderer.TEMPLATE_ID; + } +} + +export class SCMRepositoriesViewPane extends ViewPane { + + private list!: WorkbenchList; + + constructor( + options: IViewPaneOptions, + @ISCMService protected scmService: ISCMService, + @ISCMViewService protected scmViewService: ISCMViewService, + @IKeybindingService protected keybindingService: IKeybindingService, + @IContextMenuService protected contextMenuService: IContextMenuService, + @IInstantiationService instantiationService: IInstantiationService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IContextKeyService contextKeyService: IContextKeyService, + @IConfigurationService configurationService: IConfigurationService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, + @ITelemetryService telemetryService: ITelemetryService + ) { + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + } + + protected renderBody(container: HTMLElement): void { + super.renderBody(container); + + const listContainer = append(container, $('.scm-view.scm-repositories-view')); + + const delegate = new ListDelegate(); + const renderer = this.instantiationService.createInstance(RepositoryRenderer, a => this.getActionViewItem(a),); + const identityProvider = { getId: (r: ISCMRepository) => r.provider.id }; + + this.list = this.instantiationService.createInstance(WorkbenchList, `SCM Main`, listContainer, delegate, [renderer], { + identityProvider, + horizontalScrolling: false, + overrideStyles: { + listBackground: SIDE_BAR_BACKGROUND + }, + accessibilityProvider: { + getAriaLabel(r: ISCMRepository) { + return r.provider.label; + }, + getWidgetAriaLabel() { + return localize('scm', "Source Control Repositories"); + } + } + }) as WorkbenchList; + + this._register(this.list); + this._register(this.list.onDidChangeSelection(this.onListSelectionChange, this)); + this._register(this.list.onContextMenu(this.onListContextMenu, this)); + + this._register(this.scmViewService.onDidChangeVisibleRepositories(this.updateListSelection, this)); + + this._register(this.scmService.onDidAddRepository(this.onDidAddRepository, this)); + this._register(this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this)); + + for (const repository of this.scmService.repositories) { + this.onDidAddRepository(repository); + } + + if (this.orientation === Orientation.VERTICAL) { + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('scm.repositories.visible')) { + this.updateBodySize(); + } + })); + } + + this.updateListSelection(); + } + + private onDidAddRepository(repository: ISCMRepository): void { + this.list.splice(this.list.length, 0, [repository]); + this.updateBodySize(); + } + + private onDidRemoveRepository(repository: ISCMRepository): void { + const index = this.list.indexOf(repository); + + if (index > -1) { + this.list.splice(index, 1); + } + + this.updateBodySize(); + } + + focus(): void { + this.list.domFocus(); + } + + protected layoutBody(height: number, width: number): void { + super.layoutBody(height, width); + this.list.layout(height, width); + } + + private updateBodySize(): void { + if (this.orientation === Orientation.HORIZONTAL) { + return; + } + + const visibleCount = this.configurationService.getValue('scm.repositories.visible'); + const empty = this.list.length === 0; + const size = Math.min(this.list.length, visibleCount) * 22; + + this.minimumBodySize = visibleCount === 0 ? 22 : size; + this.maximumBodySize = visibleCount === 0 ? Number.POSITIVE_INFINITY : empty ? Number.POSITIVE_INFINITY : size; + } + + private onListContextMenu(e: IListContextMenuEvent): void { + if (!e.element) { + return; + } + + const provider = e.element.provider; + const menus = this.scmViewService.menus.getRepositoryMenus(provider); + const menu = menus.repositoryMenu; + const [actions, disposable] = collectContextMenuActions(menu, this.contextMenuService); + + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => actions, + getActionsContext: () => provider, + onHide() { + disposable.dispose(); + } + }); + } + + private onListSelectionChange(e: IListEvent): void { + if (e.browserEvent && e.elements.length > 0) { + const scrollTop = this.list.scrollTop; + this.scmViewService.visibleRepositories = e.elements; + this.list.scrollTop = scrollTop; + } + } + + private updateListSelection(): void { + const set = new Set(); + + for (const repository of this.scmViewService.visibleRepositories) { + set.add(repository); + } + + const selection: number[] = []; + + for (let i = 0; i < this.list.length; i++) { + if (set.has(this.list.element(i))) { + selection.push(i); + } + } + + this.list.setSelection(selection); + + if (selection.length > 0) { + this.list.setFocus([selection[0]]); + } + } + + getActionViewItem(action: IAction): IActionViewItem | undefined { + if (action instanceof StatusBarAction) { + return new StatusBarActionViewItem(action); + } + + return super.getActionViewItem(action); + } +} diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts new file mode 100644 index 00000000000..b544987ea9d --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/scm'; +import { basename } from 'vs/base/common/resources'; +import { IDisposable, Disposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; +import { append, $, addClass, toggleClass } from 'vs/base/browser/dom'; +import { ISCMRepository, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm'; +import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IAction, IActionViewItemProvider } from 'vs/base/common/actions'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { connectPrimaryMenu, isSCMRepository, StatusBarAction } from './util'; +import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; +import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; +import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; +import { FuzzyScore } from 'vs/base/common/filters'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IListRenderer } from 'vs/base/browser/ui/list/list'; + +interface RepositoryTemplate { + readonly label: HTMLElement; + readonly name: HTMLElement; + readonly description: HTMLElement; + readonly countContainer: HTMLElement; + readonly count: CountBadge; + readonly toolBar: ToolBar; + disposable: IDisposable; + readonly templateDisposable: IDisposable; +} + +export class RepositoryRenderer implements ICompressibleTreeRenderer, IListRenderer { + + static readonly TEMPLATE_ID = 'repository'; + get templateId(): string { return RepositoryRenderer.TEMPLATE_ID; } + + constructor( + private actionViewItemProvider: IActionViewItemProvider, + @ISCMViewService private scmViewService: ISCMViewService, + @ICommandService private commandService: ICommandService, + @IContextMenuService private contextMenuService: IContextMenuService, + @IThemeService private themeService: IThemeService + ) { } + + renderTemplate(container: HTMLElement): RepositoryTemplate { + // hack + if (container.classList.contains('monaco-tl-contents')) { + addClass(container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement, 'force-twistie'); + } + + const provider = append(container, $('.scm-provider')); + const label = append(provider, $('.label')); + const name = append(label, $('span.name')); + const description = append(label, $('span.description')); + const actions = append(provider, $('.actions')); + const toolBar = new ToolBar(actions, this.contextMenuService, { actionViewItemProvider: this.actionViewItemProvider }); + const countContainer = append(provider, $('.count')); + const count = new CountBadge(countContainer); + const badgeStyler = attachBadgeStyler(count, this.themeService); + const visibilityDisposable = toolBar.onDidChangeDropdownVisibility(e => toggleClass(provider, 'active', e)); + + const disposable = Disposable.None; + const templateDisposable = combinedDisposable(visibilityDisposable, toolBar, badgeStyler); + + return { label, name, description, countContainer, count, toolBar, disposable, templateDisposable }; + } + + renderElement(arg: ISCMRepository | ITreeNode, index: number, templateData: RepositoryTemplate, height: number | undefined): void { + templateData.disposable.dispose(); + + const disposables = new DisposableStore(); + const repository = isSCMRepository(arg) ? arg : arg.element; + + if (repository.provider.rootUri) { + templateData.label.title = `${repository.provider.label}: ${repository.provider.rootUri.fsPath}`; + templateData.name.textContent = basename(repository.provider.rootUri); + templateData.description.textContent = repository.provider.label; + } else { + templateData.label.title = repository.provider.label; + templateData.name.textContent = repository.provider.label; + templateData.description.textContent = ''; + } + + let statusPrimaryActions: IAction[] = []; + let menuPrimaryActions: IAction[] = []; + let menuSecondaryActions: IAction[] = []; + const updateToolbar = () => { + templateData.toolBar.setActions([...statusPrimaryActions, ...menuPrimaryActions], menuSecondaryActions); + }; + + const onDidChangeProvider = () => { + const commands = repository.provider.statusBarCommands || []; + statusPrimaryActions = commands.map(c => new StatusBarAction(c, this.commandService)); + updateToolbar(); + + const count = repository.provider.count || 0; + templateData.countContainer.setAttribute('data-count', String(count)); + templateData.count.setCount(count); + }; + disposables.add(repository.provider.onDidChange(onDidChangeProvider, null)); + onDidChangeProvider(); + + const menus = this.scmViewService.menus.getRepositoryMenus(repository.provider); + disposables.add(connectPrimaryMenu(menus.titleMenu.menu, (primary, secondary) => { + menuPrimaryActions = primary; + menuSecondaryActions = secondary; + updateToolbar(); + })); + templateData.toolBar.context = repository.provider; + + templateData.disposable = disposables; + } + + renderCompressedElements(): void { + throw new Error('Should never happen since node is incompressible'); + } + + disposeElement(group: ISCMRepository | ITreeNode, index: number, template: RepositoryTemplate): void { + template.disposable.dispose(); + } + + disposeTemplate(templateData: RepositoryTemplate): void { + templateData.disposable.dispose(); + templateData.templateDisposable.dispose(); + } +} diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index faf74c03613..1b621fc32bb 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -6,11 +6,11 @@ import 'vs/css!./media/scm'; import { Event, Emitter } from 'vs/base/common/event'; import { basename, dirname, isEqual } from 'vs/base/common/resources'; -import { IDisposable, Disposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose } from 'vs/base/common/lifecycle'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { append, $, addClass, toggleClass, removeClass, Dimension } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMService, ISCMRepository, ISCMInput, IInputValidation } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService } from 'vs/workbench/contrib/scm/common/scm'; import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -21,17 +21,16 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { MenuItemAction, IMenuService } from 'vs/platform/actions/common/actions'; import { IAction, IActionViewItem, ActionRunner, Action, RadioGroup, Separator, SubmenuAction, IActionViewItemProvider } from 'vs/base/common/actions'; -import { SCMMenus, SCMTitleMenu } from './menus'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService, LIGHT, registerThemingParticipant, IFileIconTheme } from 'vs/platform/theme/common/themeService'; -import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, connectPrimaryMenu, collectContextMenuActions } from './util'; +import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, StatusBarAction, StatusBarActionViewItem, getRepositoryVisibilityActions } from './util'; import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; import { WorkbenchCompressibleObjectTree, IOpenEvent } from 'vs/platform/list/browser/listService'; import { IConfigurationService, ConfigurationTarget, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { disposableTimeout, ThrottledDelayer } from 'vs/base/common/async'; import { ITreeNode, ITreeFilter, ITreeSorter, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; import { ResourceTree, IResourceNode } from 'vs/base/common/resourceTree'; -import { ISequence, ISplice, SimpleSequence } from 'vs/base/common/sequence'; +import { ISplice } from 'vs/base/common/sequence'; import { ICompressibleTreeRenderer, ICompressibleKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/tree/objectTree'; import { Iterable } from 'vs/base/common/iterator'; import { ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; @@ -41,7 +40,7 @@ import { compareFileNames, comparePaths } from 'vs/base/common/comparers'; import { FuzzyScore, createMatches, IMatch } from 'vs/base/common/filters'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; -import { flatten } from 'vs/base/common/arrays'; +import { coalesce, flatten } from 'vs/base/common/arrays'; import { memoize } from 'vs/base/common/decorators'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { toResource, SideBySideEditor } from 'vs/workbench/common/editor'; @@ -56,7 +55,7 @@ import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreve import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu'; import * as platform from 'vs/base/common/platform'; -import { escape, compare, format } from 'vs/base/common/strings'; +import { compare, format } from 'vs/base/common/strings'; import { inputPlaceholderForeground, inputValidationInfoBorder, inputValidationWarningBorder, inputValidationErrorBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationErrorBackground, inputValidationErrorForeground, inputBackground, inputForeground, inputBorder, focusBorder, registerColor, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; @@ -72,11 +71,9 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { ILabelService } from 'vs/platform/label/common/label'; import { KeyCode } from 'vs/base/common/keyCodes'; import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style'; -import { Command } from 'vs/editor/common/modes'; -import { renderCodicons, Codicon } from 'vs/base/common/codicons'; -import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { Codicon } from 'vs/base/common/codicons'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; -import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { RepositoryRenderer } from 'vs/workbench/contrib/scm/browser/scmRepositoryRenderer'; type TreeElement = ISCMRepository | ISCMInput | ISCMResourceGroup | IResourceNode | ISCMResource; @@ -113,145 +110,12 @@ function splitMatches(uri: URI, filterData: FuzzyScore | undefined): [IMatch[] | return [matches, descriptionMatches]; } -class StatusBarAction extends Action { - - constructor( - private command: Command, - private commandService: ICommandService - ) { - super(`statusbaraction{${command.id}}`, command.title, '', true); - this.tooltip = command.tooltip || ''; - } - - run(): Promise { - return this.commandService.executeCommand(this.command.id, ...(this.command.arguments || [])); - } -} - -class StatusBarActionViewItem extends ActionViewItem { - - constructor(action: StatusBarAction) { - super(null, action, {}); - } - - updateLabel(): void { - if (this.options.label && this.label) { - this.label.innerHTML = renderCodicons(escape(this.getAction().label)); - } - } -} - interface ISCMLayout { height: number | undefined; width: number | undefined; readonly onDidChange: Event; } -interface RepositoryTemplate { - readonly label: HTMLElement; - readonly name: HTMLElement; - readonly description: HTMLElement; - readonly countContainer: HTMLElement; - readonly count: CountBadge; - readonly toolBar: ToolBar; - disposable: IDisposable; - readonly templateDisposable: IDisposable; -} - -class RepositoryRenderer implements ICompressibleTreeRenderer { - - static readonly TEMPLATE_ID = 'repository'; - get templateId(): string { return RepositoryRenderer.TEMPLATE_ID; } - - constructor( - private actionViewItemProvider: IActionViewItemProvider, - private menus: SCMMenus, - @ICommandService private commandService: ICommandService, - @IContextMenuService private contextMenuService: IContextMenuService, - @IThemeService private themeService: IThemeService - ) { } - - renderTemplate(container: HTMLElement): RepositoryTemplate { - // hack - addClass(container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement, 'force-twistie'); - - const provider = append(container, $('.scm-provider')); - const label = append(provider, $('.label')); - const name = append(label, $('span.name')); - const description = append(label, $('span.description')); - const actions = append(provider, $('.actions')); - const toolBar = new ToolBar(actions, this.contextMenuService, { actionViewItemProvider: this.actionViewItemProvider }); - const countContainer = append(provider, $('.count')); - const count = new CountBadge(countContainer); - const badgeStyler = attachBadgeStyler(count, this.themeService); - const visibilityDisposable = toolBar.onDidChangeDropdownVisibility(e => toggleClass(provider, 'active', e)); - - const disposable = Disposable.None; - const templateDisposable = combinedDisposable(visibilityDisposable, toolBar, badgeStyler); - - return { label, name, description, countContainer, count, toolBar, disposable, templateDisposable }; - } - - renderElement(node: ITreeNode, index: number, templateData: RepositoryTemplate, height: number | undefined): void { - templateData.disposable.dispose(); - - const disposables = new DisposableStore(); - const repository = node.element; - - if (repository.provider.rootUri) { - templateData.label.title = `${repository.provider.label}: ${repository.provider.rootUri.fsPath}`; - templateData.name.textContent = basename(repository.provider.rootUri); - templateData.description.textContent = repository.provider.label; - } else { - templateData.label.title = repository.provider.label; - templateData.name.textContent = repository.provider.label; - templateData.description.textContent = ''; - } - - let statusPrimaryActions: IAction[] = []; - let menuPrimaryActions: IAction[] = []; - let menuSecondaryActions: IAction[] = []; - const updateToolbar = () => { - templateData.toolBar.setActions([...statusPrimaryActions, ...menuPrimaryActions], menuSecondaryActions); - }; - - const onDidChangeProvider = () => { - const commands = repository.provider.statusBarCommands || []; - statusPrimaryActions = commands.map(c => new StatusBarAction(c, this.commandService)); - updateToolbar(); - - const count = repository.provider.count || 0; - templateData.countContainer.setAttribute('data-count', String(count)); - templateData.count.setCount(count); - }; - disposables.add(repository.provider.onDidChange(onDidChangeProvider, null)); - onDidChangeProvider(); - - const menus = this.menus.getRepositoryMenus(repository.provider); - disposables.add(connectPrimaryMenu(menus.titleMenu.menu, (primary, secondary) => { - menuPrimaryActions = primary; - menuSecondaryActions = secondary; - updateToolbar(); - })); - templateData.toolBar.context = repository.provider; - - templateData.disposable = disposables; - } - - renderCompressedElements(): void { - throw new Error('Should never happen since node is incompressible'); - } - - disposeElement(group: ITreeNode, index: number, template: RepositoryTemplate): void { - template.disposable.dispose(); - } - - disposeTemplate(templateData: RepositoryTemplate): void { - templateData.disposable.dispose(); - templateData.templateDisposable.dispose(); - } -} - interface InputTemplate { readonly inputWidget: SCMInputWidget; disposable: IDisposable; @@ -379,7 +243,7 @@ class ResourceGroupRenderer implements ICompressibleTreeRenderer { +class ListDelegate implements IListVirtualDelegate { constructor(private readonly inputRenderer: InputRenderer) { } @@ -803,13 +667,13 @@ interface IGroupItem { readonly element: ISCMResourceGroup; readonly resources: ISCMResource[]; readonly tree: ResourceTree; - readonly disposable: IDisposable; + dispose(): void; } interface IRepositoryItem { readonly element: ISCMRepository; readonly groupItems: IGroupItem[]; - readonly disposable: IDisposable; + dispose(): void; } function isRepositoryItem(item: IRepositoryItem | IGroupItem): item is IRepositoryItem { @@ -848,7 +712,7 @@ class ViewModel { set mode(mode: ViewModelMode) { this._mode = mode; - for (const item of this.items) { + for (const [, item] of this.items) { for (const groupItem of item.groupItems) { groupItem.tree.clear(); @@ -872,7 +736,7 @@ class ViewModel { } } - private items: IRepositoryItem[] = []; + private items = new Map(); private visibilityDisposables = new DisposableStore(); private scrollTop: number | undefined; private alwaysShowRepositories = false; @@ -882,14 +746,14 @@ class ViewModel { private disposables = new DisposableStore(); constructor( - readonly repositories: ISequence, private tree: WorkbenchCompressibleObjectTree, - private menus: SCMMenus, private inputRenderer: InputRenderer, private _mode: ViewModelMode, private _sortKey: ViewModelSortKey, + @IInstantiationService protected instantiationService: IInstantiationService, @IEditorService protected editorService: IEditorService, @IConfigurationService protected configurationService: IConfigurationService, + @ISCMViewService private scmViewService: ISCMViewService ) { this.onDidChangeRepositoryCollapseState = Event.any( this._onDidChangeRepositoryCollapseState.event, @@ -907,26 +771,27 @@ class ViewModel { } } - private _onDidSpliceRepositories({ start, deleteCount, toInsert }: ISplice): void { - const itemsToInsert = toInsert.map(repository => { + private _onDidChangeVisibleRepositories({ added, removed }: ISCMViewVisibleRepositoryChangeEvent): void { + for (const repository of added) { const disposable = combinedDisposable( repository.provider.groups.onDidSplice(splice => this._onDidSpliceGroups(item, splice)), repository.input.onDidChangeVisibility(() => this.refresh(item)) ); const groupItems = repository.provider.groups.elements.map(group => this.createGroupItem(group)); - const item: IRepositoryItem = { element: repository, groupItems, disposable }; + const item: IRepositoryItem = { + element: repository, groupItems, dispose() { + dispose(this.groupItems); + disposable.dispose(); + } + }; - return item; - }); + this.items.set(repository, item); + } - const itemsToDispose = this.items.splice(start, deleteCount, ...itemsToInsert); - - for (const item of itemsToDispose) { - for (const groupItem of item.groupItems) { - groupItem.disposable.dispose(); - } - - item.disposable.dispose(); + for (const repository of removed) { + const item = this.items.get(repository)!; + item.dispose(); + this.items.delete(repository); } this.refresh(); @@ -937,7 +802,7 @@ class ViewModel { const itemsToDispose = item.groupItems.splice(start, deleteCount, ...itemsToInsert); for (const item of itemsToDispose) { - item.disposable.dispose(); + item.dispose(); } this.refresh(); @@ -951,7 +816,7 @@ class ViewModel { group.onDidSplice(splice => this._onDidSpliceGroup(item, splice)) ); - const item: IGroupItem = { element: group, resources, tree, disposable }; + const item: IGroupItem = { element: group, resources, tree, dispose() { disposable.dispose(); } }; if (this._mode === ViewModelMode.Tree) { for (const resource of resources) { @@ -987,8 +852,8 @@ class ViewModel { setVisible(visible: boolean): void { if (visible) { this.visibilityDisposables = new DisposableStore(); - this.repositories.onDidSplice(this._onDidSpliceRepositories, this, this.visibilityDisposables); - this._onDidSpliceRepositories({ start: 0, deleteCount: 0, toInsert: this.repositories.elements }); + this.scmViewService.onDidChangeVisibleRepositories(this._onDidChangeVisibleRepositories, this, this.visibilityDisposables); + this._onDidChangeVisibleRepositories({ added: this.scmViewService.visibleRepositories, removed: Iterable.empty() }); this.repositoryCollapseStates = undefined; if (typeof this.scrollTop === 'number') { @@ -999,16 +864,16 @@ class ViewModel { this.editorService.onDidActiveEditorChange(this.onDidActiveEditorChange, this, this.visibilityDisposables); this.onDidActiveEditorChange(); } else { - if (this.items.length > 1) { + if (this.items.size > 1) { this.repositoryCollapseStates = new Map(); - for (const item of this.items) { + for (const [, item] of this.items) { this.repositoryCollapseStates.set(item.element, this.tree.isCollapsed(item.element)); } } this.visibilityDisposables.dispose(); - this._onDidSpliceRepositories({ start: 0, deleteCount: this.items.length, toInsert: [] }); + this._onDidChangeVisibleRepositories({ added: Iterable.empty(), removed: [...this.items.keys()] }); this.scrollTop = this.tree.scrollTop; } @@ -1019,12 +884,14 @@ class ViewModel { private refresh(item?: IRepositoryItem | IGroupItem): void { const focusedInput = this.inputRenderer.getFocusedInput(); - if (!this.alwaysShowRepositories && (this.items.length === 1 && (!item || isRepositoryItem(item)))) { - this.tree.setChildren(null, this.render(this.items[0]).children); + if (!this.alwaysShowRepositories && (this.items.size === 1 && (!item || isRepositoryItem(item)))) { + const item = Iterable.first(this.items.values())!; + this.tree.setChildren(null, this.render(item).children); } else if (item) { this.tree.setChildren(item.element, this.render(item).children); } else { - this.tree.setChildren(null, this.items.map(item => this.render(item))); + const items = coalesce(this.scmViewService.visibleRepositories.map(r => this.items.get(r))); + this.tree.setChildren(null, items.map(item => this.render(item))); } if (focusedInput) { @@ -1043,7 +910,7 @@ class ViewModel { const children: ICompressedTreeElement[] = []; const hasSomeChanges = item.groupItems.some(item => item.element.elements.length > 0); - if (this.items.length === 1 || hasSomeChanges) { + if (this.items.size === 1 || hasSomeChanges) { if (item.element.input.visible) { children.push({ element: item.element.input, incompressible: true, collapsible: false }); } @@ -1085,8 +952,9 @@ class ViewModel { return; } - for (let i = 0; i < this.items.length; i++) { - const item = this.items[i]; + for (const repository of this.scmViewService.visibleRepositories) { + const item = this.items.get(repository)!; + // go backwards from last group for (let j = item.groupItems.length - 1; j >= 0; j--) { const groupItem = item.groupItems[j]; @@ -1105,7 +973,7 @@ class ViewModel { } focus() { - for (const repository of this.repositories.elements) { + for (const repository of this.scmViewService.visibleRepositories) { const widget = this.inputRenderer.getRenderedInputWidget(repository.input); if (widget) { @@ -1118,33 +986,33 @@ class ViewModel { } getViewActions(): IAction[] { - if (this.repositories.elements.length === 0) { - return this.menus.titleMenu.actions; + if (this.scmViewService.visibleRepositories.length === 0) { + return this.scmViewService.menus.titleMenu.actions; } - if (this.alwaysShowRepositories || this.repositories.elements.length !== 1) { + if (this.alwaysShowRepositories || this.scmViewService.visibleRepositories.length !== 1) { return []; } - const menus = this.menus.getRepositoryMenus(this.repositories.elements[0].provider); + const menus = this.scmViewService.menus.getRepositoryMenus(this.scmViewService.visibleRepositories[0].provider); return menus.titleMenu.actions; } getViewSecondaryActions(): IAction[] { - if (this.repositories.elements.length === 0) { - return this.menus.titleMenu.secondaryActions; + if (this.scmViewService.visibleRepositories.length === 0) { + return this.scmViewService.menus.titleMenu.secondaryActions; } if (!this.viewSubMenuAction) { - this.viewSubMenuAction = new SCMViewSubMenuAction(this); + this.viewSubMenuAction = this.instantiationService.createInstance(SCMViewSubMenuAction, this); this.disposables.add(this.viewSubMenuAction); } - if (this.alwaysShowRepositories || this.repositories.elements.length !== 1) { + if (this.alwaysShowRepositories || this.scmViewService.visibleRepositories.length !== 1) { return this.viewSubMenuAction.actions; } - const menus = this.menus.getRepositoryMenus(this.repositories.elements[0].provider); + const menus = this.scmViewService.menus.getRepositoryMenus(this.scmViewService.visibleRepositories[0].provider); const secondaryActions = menus.titleMenu.secondaryActions; if (secondaryActions.length === 0) { @@ -1155,19 +1023,19 @@ class ViewModel { } getViewActionsContext(): any { - if (this.repositories.elements.length === 0) { + if (this.scmViewService.visibleRepositories.length === 0) { return []; } - if (this.alwaysShowRepositories || this.repositories.elements.length !== 1) { + if (this.alwaysShowRepositories || this.scmViewService.visibleRepositories.length !== 1) { return undefined; } - return this.repositories.elements[0].provider; + return this.scmViewService.visibleRepositories[0].provider; } collapseAllProviders(): void { - for (const repository of this.repositories.elements) { + for (const repository of this.scmViewService.visibleRepositories) { if (this.tree.isCollapsible(repository)) { this.tree.collapse(repository); } @@ -1175,7 +1043,7 @@ class ViewModel { } expandAllProviders(): void { - for (const repository of this.repositories.elements) { + for (const repository of this.scmViewService.visibleRepositories) { if (this.tree.isCollapsible(repository)) { this.tree.expand(repository); } @@ -1183,52 +1051,66 @@ class ViewModel { } isAnyProviderCollapsible(): boolean { - if (!this.visible || this.repositories.elements.length === 1) { + if (!this.visible || this.scmViewService.visibleRepositories.length === 1) { return false; } - return this.repositories.elements.some(r => this.tree.hasElement(r) && this.tree.isCollapsible(r)); + return this.scmViewService.visibleRepositories.some(r => this.tree.hasElement(r) && this.tree.isCollapsible(r)); } areAllProvidersCollapsed(): boolean { - if (!this.visible || this.repositories.elements.length === 1) { + if (!this.visible || this.scmViewService.visibleRepositories.length === 1) { return false; } - return this.repositories.elements.every(r => this.tree.hasElement(r) && (!this.tree.isCollapsible(r) || this.tree.isCollapsed(r))); + return this.scmViewService.visibleRepositories.every(r => this.tree.hasElement(r) && (!this.tree.isCollapsible(r) || this.tree.isCollapsed(r))); } dispose(): void { this.visibilityDisposables.dispose(); this.disposables.dispose(); + dispose(this.items.values()); + this.items.clear(); + } +} - for (const item of this.items) { - item.disposable.dispose(); - } +class SCMViewRepositoriesSubMenuAction extends SubmenuAction { - this.items = []; + get actions(): IAction[] { + return getRepositoryVisibilityActions(this.scmService, this.scmViewService); + } + + constructor( + @ISCMService private readonly scmService: ISCMService, + @ISCMViewService private readonly scmViewService: ISCMViewService, + ) { + super('scm.repositories', localize('repositories', "Repositories"), []); } } class SCMViewSubMenuAction extends SubmenuAction { - readonly actions!: IAction[]; - - constructor(viewModel: ViewModel) { + constructor( + viewModel: ViewModel, + @IInstantiationService instantiationService: IInstantiationService + ) { const listAction = new SCMViewModeListAction(viewModel); const treeAction = new SCMViewModeTreeAction(viewModel); const sortByNameAction = new SCMSortByNameAction(viewModel); const sortByPathAction = new SCMSortByPathAction(viewModel); const sortByStatusAction = new SCMSortByStatusAction(viewModel); + const actions = [ + instantiationService.createInstance(SCMViewRepositoriesSubMenuAction), + new Separator(), + ...new RadioGroup([listAction, treeAction]).actions, + new Separator(), + ...new RadioGroup([sortByNameAction, sortByPathAction, sortByStatusAction]).actions + ]; super( 'scm.viewsort', localize('sortAction', "View & Sort"), - [ - ...new RadioGroup([listAction, treeAction]).actions, - new Separator(), - ...new RadioGroup([sortByNameAction, sortByPathAction, sortByStatusAction]).actions - ] + actions ); this._register(combinedDisposable(listAction, treeAction, sortByNameAction, sortByPathAction, sortByStatusAction)); @@ -1648,14 +1530,13 @@ export class SCMViewPane extends ViewPane { private tree!: WorkbenchCompressibleObjectTree; private viewModel!: ViewModel; private listLabels!: ResourceLabels; - private menus!: SCMMenus; private inputRenderer!: InputRenderer; - private genericTitleMenu: SCMTitleMenu; private toggleViewModelModeAction: ToggleViewModeAction | undefined; constructor( options: IViewPaneOptions, @ISCMService private scmService: ISCMService, + @ISCMViewService private scmViewService: ISCMViewService, @IKeybindingService protected keybindingService: IKeybindingService, @IThemeService protected themeService: IThemeService, @IContextMenuService protected contextMenuService: IContextMenuService, @@ -1674,8 +1555,7 @@ export class SCMViewPane extends ViewPane { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this._register(Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository)(() => this._onDidChangeViewWelcomeState.fire())); - this.genericTitleMenu = instantiationService.createInstance(SCMTitleMenu); - this._register(this.genericTitleMenu.onDidChangeTitle(this.updateActions, this)); + this._register(this.scmViewService.menus.titleMenu.onDidChangeTitle(this.updateActions, this)); } protected renderBody(container: HTMLElement): void { @@ -1696,17 +1576,10 @@ export class SCMViewPane extends ViewPane { this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.providerCountBadge'))(updateProviderCountVisibility)); updateProviderCountVisibility(); - const repositories = new SimpleSequence(this.scmService.repositories, this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository); - this._register(repositories); - - this.menus = this.instantiationService.createInstance(SCMMenus, repositories); - this._register(this.menus); - this._register(this.menus.titleMenu.onDidChangeTitle(this.updateActions, this)); - - this._register(repositories.onDidSplice(() => this.updateActions())); + this._register(this.scmViewService.onDidChangeVisibleRepositories(() => this.updateActions())); this.inputRenderer = this.instantiationService.createInstance(InputRenderer, this.layoutCache, (input, height) => this.tree.updateElementHeight(input, height)); - const delegate = new ProviderListDelegate(this.inputRenderer); + const delegate = new ListDelegate(this.inputRenderer); const actionViewItemProvider = (action: IAction) => this.getActionViewItem(action); @@ -1717,11 +1590,11 @@ export class SCMViewPane extends ViewPane { this._register(actionRunner); this._register(actionRunner.onDidBeforeRun(() => this.tree.domFocus())); - const renderers = [ - this.instantiationService.createInstance(RepositoryRenderer, actionViewItemProvider, this.menus), + const renderers: ICompressibleTreeRenderer[] = [ + this.instantiationService.createInstance(RepositoryRenderer, actionViewItemProvider), this.inputRenderer, - this.instantiationService.createInstance(ResourceGroupRenderer, actionViewItemProvider, this.menus), - this.instantiationService.createInstance(ResourceRenderer, () => this.viewModel, this.listLabels, actionViewItemProvider, actionRunner, this.menus) + this.instantiationService.createInstance(ResourceGroupRenderer, actionViewItemProvider), + this.instantiationService.createInstance(ResourceRenderer, () => this.viewModel, this.listLabels, actionViewItemProvider, actionRunner) ]; const filter = new SCMTreeFilter(); @@ -1762,7 +1635,7 @@ export class SCMViewPane extends ViewPane { viewMode = storageMode; } - this.viewModel = this.instantiationService.createInstance(ViewModel, repositories, this.tree, this.menus, this.inputRenderer, viewMode, ViewModelSortKey.Path); + this.viewModel = this.instantiationService.createInstance(ViewModel, this.tree, this.inputRenderer, viewMode, ViewModelSortKey.Path); this._register(this.viewModel); addClass(this.listContainer, 'file-icon-themable-tree'); @@ -1829,7 +1702,7 @@ export class SCMViewPane extends ViewPane { return result; } - if (this.viewModel.repositories.elements.length < 2) { + if (this.scmViewService.visibleRepositories.length < 2) { return [...result, ...this.viewModel.getViewActions()]; } @@ -1917,7 +1790,10 @@ export class SCMViewPane extends ViewPane { private onListContextMenu(e: ITreeContextMenuEvent): void { if (!e.element) { - return; + return this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => getRepositoryVisibilityActions(this.scmService, this.scmViewService) + }); } const element = e.element; @@ -1926,28 +1802,28 @@ export class SCMViewPane extends ViewPane { let disposable: IDisposable = Disposable.None; if (isSCMRepository(element)) { - const menus = this.menus.getRepositoryMenus(element.provider); - const menu = menus.getRepositoryMenu(); + const menus = this.scmViewService.menus.getRepositoryMenus(element.provider); + const menu = menus.repositoryMenu; context = element.provider; [actions, disposable] = collectContextMenuActions(menu, this.contextMenuService); } else if (isSCMInput(element)) { // noop } else if (isSCMResourceGroup(element)) { - const menus = this.menus.getRepositoryMenus(element.provider); + const menus = this.scmViewService.menus.getRepositoryMenus(element.provider); const menu = menus.getResourceGroupMenu(element); [actions, disposable] = collectContextMenuActions(menu, this.contextMenuService); } else if (ResourceTree.isResourceNode(element)) { if (element.element) { - const menus = this.menus.getRepositoryMenus(element.element.resourceGroup.provider); + const menus = this.scmViewService.menus.getRepositoryMenus(element.element.resourceGroup.provider); const menu = menus.getResourceMenu(element.element); [actions, disposable] = collectContextMenuActions(menu, this.contextMenuService); } else { - const menus = this.menus.getRepositoryMenus(element.context.provider); + const menus = this.scmViewService.menus.getRepositoryMenus(element.context.provider); const menu = menus.getResourceFolderMenu(element.context); [actions, disposable] = collectContextMenuActions(menu, this.contextMenuService); } } else { - const menus = this.menus.getRepositoryMenus(element.resourceGroup.provider); + const menus = this.scmViewService.menus.getRepositoryMenus(element.resourceGroup.provider); const menu = menus.getResourceMenu(element); [actions, disposable] = collectContextMenuActions(menu, this.contextMenuService); } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewService.ts b/src/vs/workbench/contrib/scm/browser/scmViewService.ts new file mode 100644 index 00000000000..1f2609eb975 --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/scmViewService.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 { DisposableStore } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; +import { ISCMViewService, ISCMRepository, ISCMService, ISCMViewVisibleRepositoryChangeEvent, ISCMMenus } from 'vs/workbench/contrib/scm/common/scm'; +import { Iterable } from 'vs/base/common/iterator'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { SCMMenus } from 'vs/workbench/contrib/scm/browser/menus'; + +export class SCMViewService implements ISCMViewService { + + declare readonly _serviceBrand: undefined; + + readonly menus: ISCMMenus; + + private disposables = new DisposableStore(); + + private _visibleRepositoriesSet = new Set(); + private _visibleRepositories: ISCMRepository[] = []; + + get visibleRepositories(): ISCMRepository[] { + return this._visibleRepositories; + } + + set visibleRepositories(visibleRepositories: ISCMRepository[]) { + const set = new Set(visibleRepositories); + const added = new Set(); + const removed = new Set(); + + for (const repository of visibleRepositories) { + if (!this._visibleRepositoriesSet.has(repository)) { + added.add(repository); + } + } + + for (const repository of this._visibleRepositories) { + if (!set.has(repository)) { + removed.add(repository); + } + } + + if (added.size === 0 && removed.size === 0) { + return; + } + + this._visibleRepositories = visibleRepositories; + this._visibleRepositoriesSet = set; + this._onDidSetVisibleRepositories.fire({ added, removed }); + } + + private _onDidChangeRepositories = new Emitter(); + private _onDidSetVisibleRepositories = new Emitter(); + readonly onDidChangeVisibleRepositories = Event.any( + this._onDidSetVisibleRepositories.event, + Event.debounce( + this._onDidChangeRepositories.event, + (last, e) => { + if (!last) { + return e; + } + + return { + added: Iterable.concat(last.added, e.added), + removed: Iterable.concat(last.removed, e.removed), + }; + }, 0) + ); + + constructor( + @ISCMService private readonly scmService: ISCMService, + @IInstantiationService instantiationService: IInstantiationService + ) { + this.menus = instantiationService.createInstance(SCMMenus); + + scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); + scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); + + for (const repository of scmService.repositories) { + this.onDidAddRepository(repository); + } + } + + private onDidAddRepository(repository: ISCMRepository): void { + this._visibleRepositories.push(repository); + this._visibleRepositoriesSet.add(repository); + + this._onDidChangeRepositories.fire({ added: [repository], removed: Iterable.empty() }); + } + + private onDidRemoveRepository(repository: ISCMRepository): void { + const index = this._visibleRepositories.indexOf(repository); + + if (index > -1) { + let added: Iterable = Iterable.empty(); + + this._visibleRepositories.splice(index, 1); + this._visibleRepositoriesSet.delete(repository); + + if (this._visibleRepositories.length === 0 && this.scmService.repositories.length > 0) { + const first = this.scmService.repositories[0]; + + this._visibleRepositories.push(first); + this._visibleRepositoriesSet.add(first); + added = [first]; + } + + this._onDidChangeRepositories.fire({ added, removed: [repository] }); + } + } + + isVisible(repository: ISCMRepository): boolean { + return this._visibleRepositoriesSet.has(repository); + } + + toggleVisibility(repository: ISCMRepository, visible?: boolean): void { + if (typeof visible === 'undefined') { + visible = !this.isVisible(repository); + } else if (this.isVisible(repository) === visible) { + return; + } + + if (visible) { + this.visibleRepositories = [...this.visibleRepositories, repository]; + } else { + const index = this.visibleRepositories.indexOf(repository); + + if (index > -1) { + this.visibleRepositories = [ + ...this.visibleRepositories.slice(0, index), + ...this.visibleRepositories.slice(index + 1) + ]; + } + } + } + + dispose(): void { + this.disposables.dispose(); + this._onDidChangeRepositories.dispose(); + this._onDidSetVisibleRepositories.dispose(); + } +} diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index 1b6dcfda1f7..d8b489c5fce 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -3,14 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput, ISCMService, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm'; import { IMenu } from 'vs/platform/actions/common/actions'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IDisposable, Disposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IAction } from 'vs/base/common/actions'; +import { Action, IAction } from 'vs/base/common/actions'; import { createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { equals } from 'vs/base/common/arrays'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { renderCodicons } from 'vs/base/common/codicons'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { Command } from 'vs/editor/common/modes'; +import { escape } from 'vs/base/common/strings'; +import { basename } from 'vs/base/common/resources'; +import { Iterable } from 'vs/base/common/iterator'; export function isSCMRepository(element: any): element is ISCMRepository { return !!(element as ISCMRepository).provider && typeof (element as ISCMRepository).setSelected === 'function'; @@ -74,3 +81,54 @@ export function collectContextMenuActions(menu: IMenu, contextMenuService: ICont const disposable = createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, { primary, secondary: actions }, contextMenuService, g => /^inline/.test(g)); return [actions, disposable]; } + +export class StatusBarAction extends Action { + + constructor( + private command: Command, + private commandService: ICommandService + ) { + super(`statusbaraction{${command.id}}`, command.title, '', true); + this.tooltip = command.tooltip || ''; + } + + run(): Promise { + return this.commandService.executeCommand(this.command.id, ...(this.command.arguments || [])); + } +} + +export class StatusBarActionViewItem extends ActionViewItem { + + constructor(action: StatusBarAction) { + super(null, action, {}); + } + + updateLabel(): void { + if (this.options.label && this.label) { + this.label.innerHTML = renderCodicons(escape(this.getAction().label)); + } + } +} + +export function getRepositoryVisibilityActions(scmService: ISCMService, scmViewService: ISCMViewService): IAction[] { + const visible = new Set(); + const actions = scmService.repositories.map(repository => { + const label = repository.provider.rootUri ? basename(repository.provider.rootUri) : repository.provider.label; + const action = new Action('scm.repository.toggleVisibility', label, undefined, true, async () => { + scmViewService.toggleVisibility(repository); + }); + + if (scmViewService.isVisible(repository)) { + action.checked = true; + visible.add(action); + } + + return action; + }); + + if (visible.size === 1) { + Iterable.first(visible.values())!.enabled = false; + } + + return actions; +} diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 7ee09f52d8a..aa3f57f7849 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -9,9 +9,12 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Command } from 'vs/editor/common/modes'; import { ISequence } from 'vs/base/common/sequence'; +import { IAction } from 'vs/base/common/actions'; +import { IMenu } from 'vs/platform/actions/common/actions'; export const VIEWLET_ID = 'workbench.view.scm'; export const VIEW_PANE_ID = 'workbench.scm'; +export const REPOSITORIES_VIEW_PANE_ID = 'workbench.scm.repositories'; export interface IBaselineResourceProvider { getBaselineResource(resource: URI): Promise; @@ -113,3 +116,42 @@ export interface ISCMService { registerSCMProvider(provider: ISCMProvider): ISCMRepository; } + +export interface ISCMTitleMenu { + readonly actions: IAction[]; + readonly secondaryActions: IAction[]; + readonly onDidChangeTitle: Event; + readonly menu: IMenu; +} + +export interface ISCMRepositoryMenus { + readonly titleMenu: ISCMTitleMenu; + readonly repositoryMenu: IMenu; + getResourceGroupMenu(group: ISCMResourceGroup): IMenu; + getResourceMenu(resource: ISCMResource): IMenu; + getResourceFolderMenu(group: ISCMResourceGroup): IMenu; +} + +export interface ISCMMenus { + readonly titleMenu: ISCMTitleMenu; + getRepositoryMenus(provider: ISCMProvider): ISCMRepositoryMenus; +} + +export const ISCMViewService = createDecorator('scmView'); + +export interface ISCMViewVisibleRepositoryChangeEvent { + readonly added: Iterable; + readonly removed: Iterable; +} + +export interface ISCMViewService { + readonly _serviceBrand: undefined; + + readonly menus: ISCMMenus; + + visibleRepositories: ISCMRepository[]; + readonly onDidChangeVisibleRepositories: Event; + + isVisible(repository: ISCMRepository): boolean; + toggleVisibility(repository: ISCMRepository, visible?: boolean): void; +} diff --git a/src/vs/workbench/contrib/scm/common/scmService.ts b/src/vs/workbench/contrib/scm/common/scmService.ts index fac8be966b7..6319bf1d441 100644 --- a/src/vs/workbench/contrib/scm/common/scmService.ts +++ b/src/vs/workbench/contrib/scm/common/scmService.ts @@ -7,6 +7,7 @@ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { ISCMService, ISCMProvider, ISCMInput, ISCMRepository, IInputValidator } from './scm'; import { ILogService } from 'vs/platform/log/common/log'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; class SCMInput implements ISCMInput { @@ -113,6 +114,7 @@ export class SCMService implements ISCMService { private _repositories: ISCMRepository[] = []; get repositories(): ISCMRepository[] { return [...this._repositories]; } + private providerCount: IContextKey; private _selectedRepository: ISCMRepository | undefined; private readonly _onDidSelectRepository = new Emitter(); @@ -124,7 +126,12 @@ export class SCMService implements ISCMService { private readonly _onDidRemoveProvider = new Emitter(); readonly onDidRemoveRepository: Event = this._onDidRemoveProvider.event; - constructor(@ILogService private readonly logService: ILogService) { } + constructor( + @ILogService private readonly logService: ILogService, + @IContextKeyService contextKeyService: IContextKeyService + ) { + this.providerCount = contextKeyService.createKey('scm.providerCount', 0); + } registerSCMProvider(provider: ISCMProvider): ISCMRepository { this.logService.trace('SCMService#registerSCMProvider'); @@ -150,6 +157,8 @@ export class SCMService implements ISCMService { if (this._selectedRepository === repository) { this.select(this._repositories[0]); } + + this.providerCount.set(this._repositories.length); }); const repository = new SCMRepository(provider, disposable); @@ -162,6 +171,7 @@ export class SCMService implements ISCMService { repository.setSelected(true); } + this.providerCount.set(this._repositories.length); return repository; } diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index adb459fb155..e20d202956a 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -46,7 +46,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ScrollType, IEditor, ICodeEditorViewState, IDiffEditorViewState } from 'vs/editor/common/editorCommon'; import { once } from 'vs/base/common/functional'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { getIEditor } from 'vs/editor/browser/editorBrowser'; import { withNullAsUndefined } from 'vs/base/common/types'; import { Codicon, stripCodicons } from 'vs/base/common/codicons'; @@ -134,7 +134,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider): Promise; replace(match: FileMatchOrMatch, progress?: IProgress, resource?: URI): Promise; async replace(arg: any, progress: IProgress | undefined = undefined, resource: URI | null = null): Promise { - const edits: WorkspaceTextEdit[] = this.createEdits(arg, resource); - await this.bulkEditorService.apply({ edits }, { progress }); + const edits = this.createEdits(arg, resource); + await this.bulkEditorService.apply(edits, { progress }); return Promise.all(edits.map(e => this.textFileService.files.get(e.resource)?.save())); } @@ -162,15 +161,15 @@ export class ReplaceService implements IReplaceService { const modelEdits: IIdentifiedSingleEditOperation[] = []; for (const resourceEdit of resourceEdits) { modelEdits.push(EditOperation.replaceMove( - Range.lift(resourceEdit.edit.range), - resourceEdit.edit.text) + Range.lift(resourceEdit.textEdit.range), + resourceEdit.textEdit.text) ); } replaceModel.pushEditOperations([], mergeSort(modelEdits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range)), () => []); } - private createEdits(arg: FileMatchOrMatch | FileMatch[], resource: URI | null = null): WorkspaceTextEdit[] { - const edits: WorkspaceTextEdit[] = []; + private createEdits(arg: FileMatchOrMatch | FileMatch[], resource: URI | null = null): ResourceTextEdit[] { + const edits: ResourceTextEdit[] = []; if (arg instanceof Match) { const match = arg; @@ -193,15 +192,11 @@ export class ReplaceService implements IReplaceService { return edits; } - private createEdit(match: Match, text: string, resource: URI | null = null): WorkspaceTextEdit { + private createEdit(match: Match, text: string, resource: URI | null = null): ResourceTextEdit { const fileMatch: FileMatch = match.parent(); - const resourceEdit: WorkspaceTextEdit = { - resource: resource !== null ? resource : fileMatch.resource, - edit: { - range: match.range(), - text: text - } - }; - return resourceEdit; + return new ResourceTextEdit( + resource ?? fileMatch.resource, + { range: match.range(), text }, undefined, undefined + ); } } diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 621dd65d123..b4cfa82062b 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -94,6 +94,24 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: Constants.OpenMatch, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), + primary: KeyCode.Enter, + mac: { + primary: KeyCode.Enter, + secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow] + }, + handler: (accessor) => { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + const tree: WorkbenchObjectTree = searchView.getControl(); + searchView.open(tree.getFocus()[0], false, false, true); + } + } +}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.OpenMatchToSide, weight: KeybindingWeight.WorkbenchContrib, diff --git a/src/vs/workbench/contrib/search/browser/searchActions.ts b/src/vs/workbench/contrib/search/browser/searchActions.ts index 01d1831bb0a..c5eedd23793 100644 --- a/src/vs/workbench/contrib/search/browser/searchActions.ts +++ b/src/vs/workbench/contrib/search/browser/searchActions.ts @@ -683,6 +683,8 @@ export class ReplaceAction extends AbstractSearchAndReplaceAction { static readonly LABEL = nls.localize('match.replace.label', "Replace"); + static runQ = Promise.resolve(); + constructor(private viewer: WorkbenchObjectTree, private element: Match, private viewlet: SearchView, @IReplaceService private readonly replaceService: IReplaceService, @IKeybindingService keyBindingService: IKeybindingService, @@ -691,26 +693,24 @@ export class ReplaceAction extends AbstractSearchAndReplaceAction { super(Constants.ReplaceActionId, appendKeyBindingLabel(ReplaceAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceActionId), keyBindingService), searchReplaceIcon.classNames); } - run(): Promise { + async run(): Promise { this.enabled = false; - return this.element.parent().replace(this.element).then(() => { - const elementToFocus = this.getElementToFocusAfterReplace(); - if (elementToFocus) { - this.viewer.setFocus([elementToFocus], getSelectionKeyboardEvent()); - } + await this.element.parent().replace(this.element); + const elementToFocus = this.getElementToFocusAfterReplace(); + if (elementToFocus) { + this.viewer.setFocus([elementToFocus], getSelectionKeyboardEvent()); + } - return this.getElementToShowReplacePreview(elementToFocus); - }).then(elementToShowReplacePreview => { - this.viewer.domFocus(); + const elementToShowReplacePreview = this.getElementToShowReplacePreview(elementToFocus); + this.viewer.domFocus(); - const useReplacePreview = this.configurationService.getValue().search.useReplacePreview; - if (!useReplacePreview || !elementToShowReplacePreview || this.hasToOpenFile()) { - this.viewlet.open(this.element, true); - } else { - this.replaceService.openReplacePreview(elementToShowReplacePreview, true); - } - }); + const useReplacePreview = this.configurationService.getValue().search.useReplacePreview; + if (!useReplacePreview || !elementToShowReplacePreview || this.hasToOpenFile()) { + this.viewlet.open(this.element, true); + } else { + this.replaceService.openReplacePreview(elementToShowReplacePreview, true); + } } private getElementToFocusAfterReplace(): RenderableMatch { @@ -740,11 +740,11 @@ export class ReplaceAction extends AbstractSearchAndReplaceAction { return elementToFocus!; } - private async getElementToShowReplacePreview(elementToFocus: RenderableMatch): Promise { + private getElementToShowReplacePreview(elementToFocus: RenderableMatch): Match | null { if (this.hasSameParent(elementToFocus)) { return elementToFocus; } - const previousElement = await this.getPreviousElementAfterRemoved(this.viewer, this.element); + const previousElement = this.getPreviousElementAfterRemoved(this.viewer, this.element); if (this.hasSameParent(previousElement)) { return previousElement; } diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index ad1ba249fe8..96521c068ed 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -110,6 +110,7 @@ export class SearchView extends ViewPane { private folderMatchFocused: IContextKey; private matchFocused: IContextKey; private hasSearchResultsKey: IContextKey; + private lastFocusState: 'input' | 'tree' = 'input'; private state: SearchUIState = SearchUIState.Idle; @@ -376,6 +377,9 @@ export class SearchView extends ViewPane { this.updateActions(); this.updatedActionsWhileHidden = false; } + } else { + // Reset last focus to input to preserve opening the viewlet always focusing the query editor. + this.lastFocusState = 'input'; } // Enable highlights if there are searchresults @@ -476,6 +480,7 @@ export class SearchView extends ViewPane { private trackInputBox(inputFocusTracker: dom.IFocusTracker, contextKey?: IContextKey): void { this._register(inputFocusTracker.onDidFocus(() => { + this.lastFocusState = 'input'; this.inputBoxFocused.set(true); if (contextKey) { contextKey.set(true); @@ -751,6 +756,7 @@ export class SearchView extends ViewPane { this.matchFocused.set(focus instanceof Match); this.fileMatchOrFolderMatchFocus.set(focus instanceof FileMatch || focus instanceof FolderMatch); this.fileMatchOrFolderMatchWithResourceFocus.set(focus instanceof FileMatch || focus instanceof FolderMatchWithResource); + this.lastFocusState = 'tree'; } })); @@ -874,8 +880,12 @@ export class SearchView extends ViewPane { focus(): void { super.focus(); - const updatedText = this.searchConfig.seedOnFocus ? this.updateTextFromSelection({ allowSearchOnType: false }) : false; - this.searchWidget.focus(undefined, undefined, updatedText); + if (this.lastFocusState === 'input' || !this.hasSearchResults()) { + const updatedText = this.searchConfig.seedOnFocus ? this.updateTextFromSelection({ allowSearchOnType: false }) : false; + this.searchWidget.focus(undefined, undefined, updatedText); + } else { + this.tree.domFocus(); + } } updateTextFromSelection({ allowUnselectedWord = true, allowSearchOnType = true }): boolean { diff --git a/src/vs/workbench/contrib/search/common/constants.ts b/src/vs/workbench/contrib/search/common/constants.ts index 5ac1fa1a6fd..f1e6975f788 100644 --- a/src/vs/workbench/contrib/search/common/constants.ts +++ b/src/vs/workbench/contrib/search/common/constants.ts @@ -9,6 +9,7 @@ export const FindInFilesActionId = 'workbench.action.findInFiles'; export const FocusActiveEditorCommandId = 'search.action.focusActiveEditor'; export const FocusSearchFromResults = 'search.action.focusSearchFromResults'; +export const OpenMatch = 'search.action.openResult'; export const OpenMatchToSide = 'search.action.openResultToSide'; export const CancelActionId = 'search.action.cancel'; export const RemoveActionId = 'search.action.remove'; diff --git a/src/vs/workbench/contrib/search/common/searchModel.ts b/src/vs/workbench/contrib/search/common/searchModel.ts index e5bbd712833..983355a2396 100644 --- a/src/vs/workbench/contrib/search/common/searchModel.ts +++ b/src/vs/workbench/contrib/search/common/searchModel.ts @@ -360,9 +360,12 @@ export class FileMatch extends Disposable implements IFileMatch { this._onChange.fire({ didRemove: true }); } - replace(toReplace: Match): Promise { - return this.replaceService.replace(toReplace) - .then(() => this.updatesMatchesForLineAfterReplace(toReplace.range().startLineNumber, false)); + private replaceQ = Promise.resolve(); + async replace(toReplace: Match): Promise { + return this.replaceQ = this.replaceQ.finally(async () => { + await this.replaceService.replace(toReplace); + this.updatesMatchesForLineAfterReplace(toReplace.range().startLineNumber, false); + }); } setSelectedMatch(match: Match | null): void { diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index ae1538b3ec8..4c1c7205324 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -37,7 +37,7 @@ import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; -import { EditorOptions } from 'vs/workbench/common/editor'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget'; import { SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget'; import { InputBoxFocusedKey } from 'vs/workbench/contrib/search/common/constants'; @@ -82,6 +82,7 @@ export class SearchEditor extends BaseTextEditor { private messageDisposables: IDisposable[] = []; private container: HTMLElement; private searchModel: SearchModel; + private ongoingOperations: number = 0; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -279,6 +280,14 @@ export class SearchEditor extends BaseTextEditor { } } + setQuery(query: string) { + this.queryEditorWidget.searchInput.setValue(query); + } + + selectQuery() { + this.queryEditorWidget.searchInput.select(); + } + toggleWholeWords() { this.queryEditorWidget.searchInput.setWholeWords(!this.queryEditorWidget.searchInput.getWholeWords()); this.triggerSearch({ resetCursor: false }); @@ -493,7 +502,14 @@ export class SearchEditor extends BaseTextEditor { } this.searchOperation.start(500); - await this.searchModel.search(query).finally(() => this.searchOperation.stop()); + this.ongoingOperations++; + const exit = await this.searchModel.search(query).finally(() => { + this.ongoingOperations--; + if (this.ongoingOperations === 0) { + this.searchOperation.stop(); + } + }); + const input = this.getInput(); if (!input || input !== startInput || @@ -505,7 +521,7 @@ export class SearchEditor extends BaseTextEditor { const controller = ReferencesController.get(this.searchResultEditor); controller.closeWidget(false); const labelFormatter = (uri: URI): string => this.labelService.getUriLabel(uri, { relative: true }); - const results = serializeSearchResultForEditor(this.searchModel.searchResult, config.includes, config.excludes, config.contextLines, labelFormatter, sortOrder); + const results = serializeSearchResultForEditor(this.searchModel.searchResult, config.includes, config.excludes, config.contextLines, labelFormatter, sortOrder, exit?.limitHit); const { body } = await input.getModels(); this.modelService.updateModel(body, results.text); input.config = config; @@ -540,10 +556,10 @@ export class SearchEditor extends BaseTextEditor { return this._input as SearchEditorInput; } - async setInput(newInput: SearchEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(newInput: SearchEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this.saveViewState(); - await super.setInput(newInput, options, token); + await super.setInput(newInput, options, context, token); if (token.isCancellationRequested) { return; } const { body, config } = await newInput.getModels(); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts index 0863b0b1c71..5b366415b8a 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -148,7 +148,8 @@ export const openNewSearchEditor = if (existing && args.location === 'reuse') { const input = existing.editor as SearchEditorInput; editor = assertIsDefined(await assertIsDefined(editorGroupsService.getGroup(existing.groupId)).openEditor(input)) as SearchEditor; - editor.focusSearchInput(); + if (selected) { editor.setQuery(selected); } + else { editor.selectQuery(); } } else { const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config: args, text: '' }); editor = await editorService.openEditor(input, { pinned: true }, toSide ? SIDE_GROUP : ACTIVE_GROUP) as SearchEditor; @@ -161,6 +162,8 @@ export const openNewSearchEditor = ) { editor.triggerSearch({ focusResults: args.focusResults !== false }); } + + if (args.focusResults === false) { editor.focusSearchInput(); } }; export const createEditorFromSearchResult = diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index c884a84be2b..da6858a833e 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -112,7 +112,7 @@ export class SearchEditorInput extends EditorInput { const workingCopyAdapter = new class implements IWorkingCopy { readonly resource = input.modelUri; get name() { return input.getName(); } - readonly capabilities = input.isUntitled() ? WorkingCopyCapabilities.Untitled : 0; + readonly capabilities = input.isUntitled() ? WorkingCopyCapabilities.Untitled : WorkingCopyCapabilities.None; readonly onDidChangeDirty = input.onDidChangeDirty; readonly onDidChangeContent = input.onDidChangeContent; isDirty(): boolean { return input.isDirty(); } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts index 1bb2abc19ab..31d48055864 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts @@ -208,7 +208,7 @@ export const extractSearchQueryFromLines = (lines: string[]): SearchConfiguratio }; export const serializeSearchResultForEditor = - (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, contextLines: number, labelFormatter: (x: URI) => string, sortOrder: SearchSortOrder): { matchRanges: Range[], text: string, config: Partial } => { + (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, contextLines: number, labelFormatter: (x: URI) => string, sortOrder: SearchSortOrder, limitHit?: boolean): { matchRanges: Range[], text: string, config: Partial } => { if (!searchResult.query) { throw Error('Internal Error: Expected query, got null'); } const config = contentPatternToSearchConfiguration(searchResult.query, rawIncludePattern, rawExcludePattern, contextLines); @@ -219,7 +219,11 @@ export const serializeSearchResultForEditor = searchResult.count() ? `${resultcount} - ${filecount}` : localize('noResults', "No Results"), - '']; + ]; + if (limitHit) { + info.push(localize('searchMaxResultsWarning', "The result set only contains a subset of all matches. Please be more specific in your search to narrow down the results.")); + } + info.push(''); const matchComparer = (a: FileMatch | FolderMatch, b: FileMatch | FolderMatch) => searchMatchComparer(a, b, sortOrder); diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index e757824eb43..883e2ffcb85 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -1528,7 +1528,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (this._taskSystem?.isTaskVisible(executeResult.task)) { const message = nls.localize('TaskSystem.activeSame.noBackground', 'The task \'{0}\' is already active.', executeResult.task.getQualifiedLabel()); let lastInstance = this.getTaskSystem().getLastInstance(executeResult.task) ?? executeResult.task; - this.notificationService.prompt(Severity.Info, message, + this.notificationService.prompt(Severity.Warning, message, [{ label: nls.localize('terminateTask', "Terminate Task"), run: () => this.terminate(lastInstance) @@ -2606,7 +2606,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } if (buildTasks.length === 1) { this.tryResolveTask(buildTasks[0]).then(resolvedTask => { - this.run(resolvedTask).then(undefined, reason => { + this.run(resolvedTask, undefined, TaskRunSource.User).then(undefined, reason => { // eat the error, it has already been surfaced to the user and we don't care about it here }); }); @@ -2617,7 +2617,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (tasks.length > 0) { let { defaults, users } = this.splitPerGroupType(tasks); if (defaults.length === 1) { - this.run(defaults[0]).then(undefined, reason => { + this.run(defaults[0], undefined, TaskRunSource.User).then(undefined, reason => { // eat the error, it has already been surfaced to the user and we don't care about it here }); return; @@ -2641,7 +2641,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this.runConfigureDefaultBuildTask(); return; } - this.run(task, { attachProblemMatcher: true }).then(undefined, reason => { + this.run(task, { attachProblemMatcher: true }, TaskRunSource.User).then(undefined, reason => { // eat the error, it has already been surfaced to the user and we don't care about it here }); }); @@ -2667,7 +2667,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (tasks.length > 0) { let { defaults, users } = this.splitPerGroupType(tasks); if (defaults.length === 1) { - this.run(defaults[0]).then(undefined, reason => { + this.run(defaults[0], undefined, TaskRunSource.User).then(undefined, reason => { // eat the error, it has already been surfaced to the user and we don't care about it here }); return; @@ -2691,7 +2691,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this.runConfigureTasks(); return; } - this.run(task).then(undefined, reason => { + this.run(task, undefined, TaskRunSource.User).then(undefined, reason => { // eat the error, it has already been surfaced to the user and we don't care about it here }); }); @@ -2963,7 +2963,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (tasks.length > 0) { tasks = tasks.sort((a, b) => a._label.localeCompare(b._label)); for (let task of tasks) { - entries.push({ label: task._label, task, description: this.getTaskDescription(task) }); + entries.push({ label: task._label, task, description: this.getTaskDescription(task), detail: this.showDetail() ? task.configurationProperties.detail : undefined }); if (!ContributedTask.is(task)) { needsCreateOrOpen = false; } @@ -3059,7 +3059,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (selectedTask) { selectedEntry = { label: nls.localize('TaskService.defaultBuildTaskExists', '{0} is already marked as the default build task', selectedTask.getQualifiedLabel()), - task: selectedTask + task: selectedTask, + detail: this.showDetail() ? selectedTask.configurationProperties.detail : undefined }; } this.showIgnoredFoldersMessage().then(() => { @@ -3110,7 +3111,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (selectedTask) { selectedEntry = { label: nls.localize('TaskService.defaultTestTaskExists', '{0} is already marked as the default test task.', selectedTask.getQualifiedLabel()), - task: selectedTask + task: selectedTask, + detail: this.showDetail() ? selectedTask.configurationProperties.detail : undefined }; } diff --git a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts index 4866dd1e823..c17c12d9b76 100644 --- a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts +++ b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts @@ -8,11 +8,12 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { ITaskService, WorkspaceFolderTaskResult } from 'vs/workbench/contrib/tasks/common/taskService'; import { forEach } from 'vs/base/common/collections'; -import { RunOnOptions, Task, TaskRunSource } from 'vs/workbench/contrib/tasks/common/tasks'; +import { RunOnOptions, Task, TaskRunSource, TASKS_CATEGORY } from 'vs/workbench/contrib/tasks/common/tasks'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { Action } from 'vs/base/common/actions'; import { IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { Action2 } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; const ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE = 'tasks.run.allowAutomatic'; @@ -132,27 +133,29 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut } -export class ManageAutomaticTaskRunning extends Action { +export class ManageAutomaticTaskRunning extends Action2 { public static readonly ID = 'workbench.action.tasks.manageAutomaticRunning'; public static readonly LABEL = nls.localize('workbench.action.tasks.manageAutomaticRunning', "Manage Automatic Tasks in Folder"); - constructor( - id: string, label: string, - @IStorageService private readonly storageService: IStorageService, - @IQuickInputService private readonly quickInputService: IQuickInputService - ) { - super(id, label); + constructor() { + super({ + id: ManageAutomaticTaskRunning.ID, + title: ManageAutomaticTaskRunning.LABEL, + category: TASKS_CATEGORY + }); } - public async run(): Promise { + public async run(accessor: ServicesAccessor): Promise { + const quickInputService = accessor.get(IQuickInputService); + const storageService = accessor.get(IStorageService); const allowItem: IQuickPickItem = { label: nls.localize('workbench.action.tasks.allowAutomaticTasks', "Allow Automatic Tasks in Folder") }; const disallowItem: IQuickPickItem = { label: nls.localize('workbench.action.tasks.disallowAutomaticTasks', "Disallow Automatic Tasks in Folder") }; - const value = await this.quickInputService.pick([allowItem, disallowItem], { canPickMany: false }); + const value = await quickInputService.pick([allowItem, disallowItem], { canPickMany: false }); if (!value) { return; } - this.storageService.store(ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE, value === allowItem, StorageScope.WORKSPACE); + storageService.store(ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE, value === allowItem, StorageScope.WORKSPACE); } } diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index 6418a23ec93..25e5462bf69 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import { Disposable } from 'vs/base/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { MenuRegistry, MenuId, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ProblemMatcherRegistry } from 'vs/workbench/contrib/tasks/common/problemMatcher'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; @@ -20,11 +20,10 @@ import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatus import { IOutputChannelRegistry, Extensions as OutputExt } from 'vs/workbench/services/output/common/output'; -import { TaskEvent, TaskEventKind, TaskGroup, TASK_RUNNING_STATE } from 'vs/workbench/contrib/tasks/common/tasks'; -import { ITaskService } from 'vs/workbench/contrib/tasks/common/taskService'; +import { TaskEvent, TaskEventKind, TaskGroup, TASKS_CATEGORY, TASK_RUNNING_STATE } from 'vs/workbench/contrib/tasks/common/tasks'; +import { ITaskService, ProcessExecutionSupportedContext, ShellExecutionSupportedContext } from 'vs/workbench/contrib/tasks/common/taskService'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { RunAutomaticTasks, ManageAutomaticTaskRunning } from 'vs/workbench/contrib/tasks/browser/runAutomaticTasks'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; @@ -36,14 +35,22 @@ import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'v import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; import { TasksQuickAccessProvider } from 'vs/workbench/contrib/tasks/browser/tasksQuickAccess'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -let tasksCategory = { value: nls.localize('tasksCategory', "Tasks"), original: 'Tasks' }; +const SHOW_TASKS_COMMANDS_CONTEXT = ContextKeyExpr.or(ShellExecutionSupportedContext, ProcessExecutionSupportedContext); const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(RunAutomaticTasks, LifecyclePhase.Eventually); -const actionRegistry = Registry.as(ActionExtensions.WorkbenchActions); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ManageAutomaticTaskRunning), 'Tasks: Manage Automatic Tasks in Folder', tasksCategory.value); +registerAction2(ManageAutomaticTaskRunning); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: ManageAutomaticTaskRunning.ID, + title: ManageAutomaticTaskRunning.LABEL, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); export class TaskStatusBarContributions extends Disposable implements IWorkbenchContribution { private runningTasksStatusItem: IStatusbarEntryAccessor | undefined; @@ -160,7 +167,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { id: 'workbench.action.tasks.runTask', title: nls.localize({ key: 'miRunTask', comment: ['&& denotes a mnemonic'] }, "&&Run Task...") }, - order: 1 + order: 1, + when: SHOW_TASKS_COMMANDS_CONTEXT }); MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { @@ -169,7 +177,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { id: 'workbench.action.tasks.build', title: nls.localize({ key: 'miBuildTask', comment: ['&& denotes a mnemonic'] }, "Run &&Build Task...") }, - order: 2 + order: 2, + when: SHOW_TASKS_COMMANDS_CONTEXT }); // Manage Tasks @@ -180,7 +189,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { id: 'workbench.action.tasks.showTasks', title: nls.localize({ key: 'miRunningTask', comment: ['&& denotes a mnemonic'] }, "Show Runnin&&g Tasks...") }, - order: 1 + order: 1, + when: SHOW_TASKS_COMMANDS_CONTEXT }); MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { @@ -190,7 +200,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { id: 'workbench.action.tasks.restartTask', title: nls.localize({ key: 'miRestartTask', comment: ['&& denotes a mnemonic'] }, "R&&estart Running Task...") }, - order: 2 + order: 2, + when: SHOW_TASKS_COMMANDS_CONTEXT }); MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { @@ -200,7 +211,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { id: 'workbench.action.tasks.terminate', title: nls.localize({ key: 'miTerminateTask', comment: ['&& denotes a mnemonic'] }, "&&Terminate Task...") }, - order: 3 + order: 3, + when: SHOW_TASKS_COMMANDS_CONTEXT }); // Configure Tasks @@ -210,7 +222,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { id: 'workbench.action.tasks.configureTaskRunner', title: nls.localize({ key: 'miConfigureTask', comment: ['&& denotes a mnemonic'] }, "&&Configure Tasks...") }, - order: 1 + order: 1, + when: SHOW_TASKS_COMMANDS_CONTEXT }); MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { @@ -219,31 +232,123 @@ MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { id: 'workbench.action.tasks.configureDefaultBuildTask', title: nls.localize({ key: 'miConfigureBuildTask', comment: ['&& denotes a mnemonic'] }, "Configure De&&fault Build Task...") }, - order: 2 + order: 2, + when: SHOW_TASKS_COMMANDS_CONTEXT }); -MenuRegistry.appendMenuItem(MenuId.CommandPalette, ({ +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: 'workbench.action.tasks.openWorkspaceFileTasks', title: { value: nls.localize('workbench.action.tasks.openWorkspaceFileTasks', "Open Workspace Tasks"), original: 'Open Workspace Tasks' }, - category: tasksCategory + category: TASKS_CATEGORY }, - when: WorkbenchStateContext.isEqualTo('workspace') -})); + when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), SHOW_TASKS_COMMANDS_CONTEXT) +}); -MenuRegistry.addCommand({ id: ConfigureTaskAction.ID, title: { value: ConfigureTaskAction.TEXT, original: 'Configure Task' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.showLog', title: { value: nls.localize('ShowLogAction.label', "Show Task Log"), original: 'Show Task Log' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.runTask', title: { value: nls.localize('RunTaskAction.label', "Run Task"), original: 'Run Task' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.reRunTask', title: { value: nls.localize('ReRunTaskAction.label', "Rerun Last Task"), original: 'Rerun Last Task' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.restartTask', title: { value: nls.localize('RestartTaskAction.label', "Restart Running Task"), original: 'Restart Running Task' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.showTasks', title: { value: nls.localize('ShowTasksAction.label', "Show Running Tasks"), original: 'Show Running Tasks' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.terminate', title: { value: nls.localize('TerminateAction.label', "Terminate Task"), original: 'Terminate Task' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.build', title: { value: nls.localize('BuildAction.label', "Run Build Task"), original: 'Run Build Task' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.test', title: { value: nls.localize('TestAction.label', "Run Test Task"), original: 'Run Test Task' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.configureDefaultBuildTask', title: { value: nls.localize('ConfigureDefaultBuildTask.label', "Configure Default Build Task"), original: 'Configure Default Build Task' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.configureDefaultTestTask', title: { value: nls.localize('ConfigureDefaultTestTask.label', "Configure Default Test Task"), original: 'Configure Default Test Task' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.openUserTasks', title: { value: nls.localize('workbench.action.tasks.openUserTasks', "Open User Tasks"), original: 'Open User Tasks' }, category: tasksCategory }); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: ConfigureTaskAction.ID, + title: { value: ConfigureTaskAction.TEXT, original: 'Configure Task' }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.showLog', + title: { value: nls.localize('ShowLogAction.label', "Show Task Log"), original: 'Show Task Log' }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.runTask', + title: { value: nls.localize('RunTaskAction.label', "Run Task"), original: 'Run Task' }, + category: TASKS_CATEGORY + } +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.reRunTask', + title: { value: nls.localize('ReRunTaskAction.label', "Rerun Last Task"), original: 'Rerun Last Task' }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.restartTask', + title: { value: nls.localize('RestartTaskAction.label', "Restart Running Task"), original: 'Restart Running Task' }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.showTasks', + title: { value: nls.localize('ShowTasksAction.label', "Show Running Tasks"), original: 'Show Running Tasks' }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.terminate', + title: { value: nls.localize('TerminateAction.label', "Terminate Task"), original: 'Terminate Task' }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.build', + title: { value: nls.localize('BuildAction.label', "Run Build Task"), original: 'Run Build Task' }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.test', + title: { value: nls.localize('TestAction.label', "Run Test Task"), original: 'Run Test Task' }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.configureDefaultBuildTask', + title: { + value: nls.localize('ConfigureDefaultBuildTask.label', "Configure Default Build Task"), + original: 'Configure Default Build Task' + }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.configureDefaultTestTask', + title: { + value: nls.localize('ConfigureDefaultTestTask.label', "Configure Default Test Task"), + original: 'Configure Default Test Task' + }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.openUserTasks', + title: { + value: nls.localize('workbench.action.tasks.openUserTasks', "Open User Tasks"), + original: 'Open User Tasks' + }, category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); // MenuRegistry.addCommand( { id: 'workbench.action.tasks.rebuild', title: nls.localize('RebuildAction.label', 'Run Rebuild Task'), category: tasksCategory }); // MenuRegistry.addCommand( { id: 'workbench.action.tasks.clean', title: nls.localize('CleanAction.label', 'Run Clean Task'), category: tasksCategory }); diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index f6993a12c3c..e35865f75fc 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -81,12 +81,12 @@ class InstanceManager { class VariableResolver { - constructor(public workspaceFolder: IWorkspaceFolder | undefined, public taskSystemInfo: TaskSystemInfo | undefined, private _values: Map, private _service: IConfigurationResolverService | undefined) { + constructor(public workspaceFolder: IWorkspaceFolder | undefined, public taskSystemInfo: TaskSystemInfo | undefined, public readonly values: Map, private _service: IConfigurationResolverService | undefined) { } resolve(value: string): string { return value.replace(/\$\{(.*?)\}/g, (match: string, variable: string) => { // Strip out the ${} because the map contains them variables without those characters. - let result = this._values.get(match.substring(2, match.length - 1)); + let result = this.values.get(match.substring(2, match.length - 1)); if ((result !== undefined) && (result !== null)) { return result; } @@ -840,7 +840,7 @@ export class TerminalTaskSystem implements ITaskSystem { }, (_error) => { // The process never got ready. Need to think how to handle this. }); - this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Start, task, terminal.id)); + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Start, task, terminal.id, resolver.values)); const mapKey = task.getMapKey(); this.busyTasks[mapKey] = task; this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Active, task)); @@ -1331,6 +1331,22 @@ export class TerminalTaskSystem implements ITaskSystem { this.collectCommandVariables(variables, task.command, task); } this.collectMatcherVariables(variables, task.configurationProperties.problemMatchers); + + if (task.command.runtime === RuntimeType.CustomExecution && CustomTask.is(task)) { + this.collectDefinitionVariables(variables, task._source.config.element); + } + } + + private collectDefinitionVariables(variables: Set, definition: any): void { + for (const key in definition) { + if (Types.isString(definition[key])) { + this.collectVariables(variables, definition[key]); + } else if (Types.isArray(definition[key])) { + definition[key].forEach((element: any) => this.collectDefinitionVariables(variables, element)); + } else if (Types.isObject(definition[key])) { + this.collectDefinitionVariables(variables, definition[key]); + } + } } private collectCommandVariables(variables: Set, command: CommandConfiguration, task: CustomTask | ContributedTask): void { diff --git a/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts b/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts index 38ca7e709a8..f1afbaf7fdd 100644 --- a/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts +++ b/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts @@ -250,8 +250,8 @@ const command: IJSONSchema = { enum: ['escape', 'strong', 'weak'], enumDescriptions: [ nls.localize('JsonSchema.tasks.quoting.escape', 'Escapes characters using the shell\'s escape character (e.g. ` under PowerShell and \\ under bash).'), - nls.localize('JsonSchema.tasks.quoting.strong', 'Quotes the argument using the shell\'s strong quote character (e.g. " under PowerShell and bash).'), - nls.localize('JsonSchema.tasks.quoting.weak', 'Quotes the argument using the shell\'s weak quote character (e.g. \' under PowerShell and bash).'), + nls.localize('JsonSchema.tasks.quoting.strong', 'Quotes the argument using the shell\'s strong quote character (e.g. \' under PowerShell and bash).'), + nls.localize('JsonSchema.tasks.quoting.weak', 'Quotes the argument using the shell\'s weak quote character (e.g. " under PowerShell and bash).'), ], default: 'strong', description: nls.localize('JsonSchema.command.quotesString.quote', 'How the command value should be quoted.') @@ -283,8 +283,8 @@ const args: IJSONSchema = { enum: ['escape', 'strong', 'weak'], enumDescriptions: [ nls.localize('JsonSchema.tasks.quoting.escape', 'Escapes characters using the shell\'s escape character (e.g. ` under PowerShell and \\ under bash).'), - nls.localize('JsonSchema.tasks.quoting.strong', 'Quotes the argument using the shell\'s strong quote character (e.g. " under PowerShell and bash).'), - nls.localize('JsonSchema.tasks.quoting.weak', 'Quotes the argument using the shell\'s weak quote character (e.g. \' under PowerShell and bash).'), + nls.localize('JsonSchema.tasks.quoting.strong', 'Quotes the argument using the shell\'s strong quote character (e.g. \' under PowerShell and bash).'), + nls.localize('JsonSchema.tasks.quoting.weak', 'Quotes the argument using the shell\'s weak quote character (e.g. " under PowerShell and bash).'), ], default: 'strong', description: nls.localize('JsonSchema.args.quotesString.quote', 'How the argument value should be quoted.') diff --git a/src/vs/workbench/contrib/tasks/common/tasks.ts b/src/vs/workbench/contrib/tasks/common/tasks.ts index ec40a24a548..a88910f481f 100644 --- a/src/vs/workbench/contrib/tasks/common/tasks.ts +++ b/src/vs/workbench/contrib/tasks/common/tasks.ts @@ -19,6 +19,7 @@ import { ConfigurationTarget } from 'vs/platform/configuration/common/configurat import { USER_TASKS_GROUP_KEY } from 'vs/workbench/contrib/tasks/common/taskService'; export const TASK_RUNNING_STATE = new RawContextKey('taskRunning', false); +export const TASKS_CATEGORY = { value: nls.localize('tasksCategory', "Tasks"), original: 'Tasks' }; export enum ShellQuoting { /** @@ -1066,6 +1067,7 @@ export interface TaskEvent { exitCode?: number; terminalId?: number; __task?: Task; + resolvedVariables?: Map; } export const enum TaskRunSource { @@ -1077,10 +1079,10 @@ export const enum TaskRunSource { export namespace TaskEvent { export function create(kind: TaskEventKind.ProcessStarted | TaskEventKind.ProcessEnded, task: Task, processIdOrExitCode?: number): TaskEvent; - export function create(kind: TaskEventKind.Start, task: Task, terminalId?: number): TaskEvent; + export function create(kind: TaskEventKind.Start, task: Task, terminalId?: number, resolvedVariables?: Map): TaskEvent; export function create(kind: TaskEventKind.DependsOnStarted | TaskEventKind.Start | TaskEventKind.Active | TaskEventKind.Inactive | TaskEventKind.Terminated | TaskEventKind.End, task: Task): TaskEvent; export function create(kind: TaskEventKind.Changed): TaskEvent; - export function create(kind: TaskEventKind, task?: Task, processIdOrExitCodeOrTerminalId?: number): TaskEvent { + export function create(kind: TaskEventKind, task?: Task, processIdOrExitCodeOrTerminalId?: number, resolvedVariables?: Map): TaskEvent { if (task) { let result: TaskEvent = { kind: kind, @@ -1095,6 +1097,7 @@ export namespace TaskEvent { }; if (kind === TaskEventKind.Start) { result.terminalId = processIdOrExitCodeOrTerminalId; + result.resolvedVariables = resolvedVariables; } else if (kind === TaskEventKind.ProcessStarted) { result.processId = processIdOrExitCodeOrTerminalId; } else if (kind === TaskEventKind.ProcessEnded) { diff --git a/src/vs/workbench/contrib/terminal/browser/addons/commandTrackerAddon.ts b/src/vs/workbench/contrib/terminal/browser/addons/commandTrackerAddon.ts index c81d1e9cc82..01c90349d9e 100644 --- a/src/vs/workbench/contrib/terminal/browser/addons/commandTrackerAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/addons/commandTrackerAddon.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Terminal, IMarker, ITerminalAddon } from 'xterm'; +import type { Terminal, IMarker, ITerminalAddon } from 'xterm'; import { ICommandTracker } from 'vs/workbench/contrib/terminal/common/terminal'; /** diff --git a/src/vs/workbench/contrib/terminal/browser/addons/navigationModeAddon.ts b/src/vs/workbench/contrib/terminal/browser/addons/navigationModeAddon.ts index baba436e968..8487459af57 100644 --- a/src/vs/workbench/contrib/terminal/browser/addons/navigationModeAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/addons/navigationModeAddon.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { Terminal, ITerminalAddon } from 'xterm'; +import type { Terminal, ITerminalAddon } from 'xterm'; import { addDisposableListener } from 'vs/base/browser/dom'; import { INavigationMode } from 'vs/workbench/contrib/terminal/common/terminal'; diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalBaseLinkProvider.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalBaseLinkProvider.ts index 38301d0e32c..3adccc7f5c2 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalBaseLinkProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalBaseLinkProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ILinkProvider, ILink } from 'xterm'; +import type { ILinkProvider, ILink } from 'xterm'; import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink'; export abstract class TerminalBaseLinkProvider implements ILinkProvider { diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkProviderAdapter.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkProviderAdapter.ts index 40478f7f7f9..fce440f2fd6 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkProviderAdapter.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkProviderAdapter.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Terminal, IViewportRange, IBufferLine } from 'xterm'; +import type { Terminal, IViewportRange, IBufferLine } from 'xterm'; import { getXtermLineContent, convertLinkRangeToBuffer } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers'; import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLink.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLink.ts index d0d122fae55..a5b5b340215 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLink.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLink.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IViewportRange, IBufferRange, ILink, ILinkDecorations } from 'xterm'; +import type { IViewportRange, IBufferRange, ILink, ILinkDecorations } from 'xterm'; import { DisposableStore } from 'vs/base/common/lifecycle'; import * as dom from 'vs/base/browser/dom'; import { RunOnceScheduler } from 'vs/base/common/async'; diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts index de8cb63d107..f2b85d1e3b5 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IViewportRange, IBufferRange, IBufferLine, IBuffer, IBufferCellPosition } from 'xterm'; +import type { IViewportRange, IBufferRange, IBufferLine, IBuffer, IBufferCellPosition } from 'xterm'; import { IRange } from 'vs/editor/common/core/range'; export function convertLinkRangeToBuffer(lines: IBufferLine[], bufferWidth: number, range: IRange, startLine: number) { @@ -21,10 +21,16 @@ export function convertLinkRangeToBuffer(lines: IBufferLine[], bufferWidth: numb // Shift start range right for each wide character before the link let startOffset = 0; const startWrappedLineCount = Math.ceil(range.startColumn / bufferWidth); - for (let y = 0; y < startWrappedLineCount; y++) { + for (let y = 0; y < Math.min(startWrappedLineCount); y++) { const lineLength = Math.min(bufferWidth, range.startColumn - y * bufferWidth); let lineOffset = 0; const line = lines[y]; + // Sanity check for line, apparently this can happen but it's not clear under what + // circumstances this happens. Continue on, skipping the remainder of start offset if this + // happens to minimize impact. + if (!line) { + break; + } for (let x = 0; x < Math.min(bufferWidth, lineLength + lineOffset); x++) { const cell = line.getCell(x)!; const width = cell.getWidth(); @@ -48,6 +54,12 @@ export function convertLinkRangeToBuffer(lines: IBufferLine[], bufferWidth: numb const startLineOffset = (y === startWrappedLineCount - 1 ? startOffset : 0); let lineOffset = 0; const line = lines[y]; + // Sanity check for line, apparently this can happen but it's not clear under what + // circumstances this happens. Continue on, skipping the remainder of start offset if this + // happens to minimize impact. + if (!line) { + break; + } for (let x = start; x < Math.min(bufferWidth, lineLength + lineOffset + startLineOffset); x++) { const cell = line.getCell(x)!; const width = cell.getWidth(); diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts index 64434e13548..25ecbac5a6f 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts @@ -13,14 +13,12 @@ import { ITerminalProcessManager, ITerminalConfiguration, TERMINAL_CONFIG_SECTIO import { ITextEditorSelection } from 'vs/platform/editor/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IFileService } from 'vs/platform/files/common/files'; -import { Terminal, IViewportRange, ILinkProvider } from 'xterm'; +import type { Terminal, IViewportRange, ILinkProvider } from 'xterm'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { posix, win32 } from 'vs/base/common/path'; -import { ITerminalBeforeHandleLinkEvent, LINK_INTERCEPT_THRESHOLD, ITerminalExternalLinkProvider, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalExternalLinkProvider, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { OperatingSystem, isMacintosh, OS } from 'vs/base/common/platform'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; -import { Emitter, Event } from 'vs/base/common/event'; -import { ILogService } from 'vs/platform/log/common/log'; import { TerminalProtocolLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider'; import { TerminalValidatedLocalLinkProvider, lineAndColumnClause, unixLocalLinkClause, winLocalLinkClause, winDrivePrefix, winLineAndColumnMatchIndex, unixLineAndColumnMatchIndex, lineAndColumnClauseGroupCount } from 'vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider'; import { TerminalWordLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider'; @@ -44,24 +42,9 @@ interface IPath { export class TerminalLinkManager extends DisposableStore { private _widgetManager: TerminalWidgetManager | undefined; private _processCwd: string | undefined; - private _hasBeforeHandleLinkListeners = false; private _standardLinkProviders: ILinkProvider[] = []; private _standardLinkProvidersDisposables: IDisposable[] = []; - protected static _LINK_INTERCEPT_THRESHOLD = LINK_INTERCEPT_THRESHOLD; - public static readonly LINK_INTERCEPT_THRESHOLD = TerminalLinkManager._LINK_INTERCEPT_THRESHOLD; - - private readonly _onBeforeHandleLink = this.add(new Emitter({ - onFirstListenerAdd: () => this._hasBeforeHandleLinkListeners = true, - onLastListenerRemove: () => this._hasBeforeHandleLinkListeners = false - })); - /** - * Allows intercepting links and handling them outside of the default link handler. When fired - * the listener has a set amount of time to handle the link or the default handler will fire. - * This was designed to only be handled by a single listener. - */ - public get onBeforeHandleLink(): Event { return this._onBeforeHandleLink.event; } - constructor( private _xterm: Terminal, private readonly _processManager: ITerminalProcessManager, @@ -69,14 +52,13 @@ export class TerminalLinkManager extends DisposableStore { @IEditorService private readonly _editorService: IEditorService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IFileService private readonly _fileService: IFileService, - @ILogService private readonly _logService: ILogService, @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); // Protocol links const wrappedActivateCallback = this._wrapLinkHandler((_, link) => this._handleProtocolLink(link)); - const protocolProvider = this._instantiationService.createInstance(TerminalProtocolLinkProvider, this._xterm, wrappedActivateCallback, this._tooltipCallback2.bind(this)); + const protocolProvider = this._instantiationService.createInstance(TerminalProtocolLinkProvider, this._xterm, wrappedActivateCallback, this._tooltipCallback.bind(this)); this._standardLinkProviders.push(protocolProvider); // Validated local links @@ -87,19 +69,19 @@ export class TerminalLinkManager extends DisposableStore { this._processManager.os || OS, wrappedTextLinkActivateCallback, this._wrapLinkHandler.bind(this), - this._tooltipCallback2.bind(this), + this._tooltipCallback.bind(this), async (link, cb) => cb(await this._resolvePath(link))); this._standardLinkProviders.push(validatedProvider); } // Word links - const wordProvider = this._instantiationService.createInstance(TerminalWordLinkProvider, this._xterm, this._wrapLinkHandler.bind(this), this._tooltipCallback2.bind(this)); + const wordProvider = this._instantiationService.createInstance(TerminalWordLinkProvider, this._xterm, this._wrapLinkHandler.bind(this), this._tooltipCallback.bind(this)); this._standardLinkProviders.push(wordProvider); this._registerStandardLinkProviders(); } - private _tooltipCallback2(link: TerminalLink, viewportRange: IViewportRange, modifierDownCallback?: () => void, modifierUpCallback?: () => void) { + private _tooltipCallback(link: TerminalLink, viewportRange: IViewportRange, modifierDownCallback?: () => void, modifierUpCallback?: () => void) { if (!this._widgetManager) { return; } @@ -156,7 +138,7 @@ export class TerminalLinkManager extends DisposableStore { } public registerExternalLinkProvider(instance: ITerminalInstance, linkProvider: ITerminalExternalLinkProvider): IDisposable { - const wrappedLinkProvider = this._instantiationService.createInstance(TerminalExternalLinkProviderAdapter, this._xterm, instance, linkProvider, this._wrapLinkHandler.bind(this), this._tooltipCallback2.bind(this)); + const wrappedLinkProvider = this._instantiationService.createInstance(TerminalExternalLinkProviderAdapter, this._xterm, instance, linkProvider, this._wrapLinkHandler.bind(this), this._tooltipCallback.bind(this)); const newLinkProvider = this._xterm.registerLinkProvider(wrappedLinkProvider); // Re-register the standard link providers so they are a lower priority that the new one this._registerStandardLinkProviders(); @@ -173,38 +155,11 @@ export class TerminalLinkManager extends DisposableStore { return; } - // Allow the link to be intercepted if there are listeners - if (this._hasBeforeHandleLinkListeners) { - const wasHandled = await this._triggerBeforeHandleLinkListeners(link); - if (!wasHandled) { - handler(event, link); - } - return; - } - // Just call the handler if there is no before listener handler(event, link); }; } - private async _triggerBeforeHandleLinkListeners(link: string): Promise { - return new Promise(r => { - const timeoutId = setTimeout(() => { - canceled = true; - this._logService.error(`An extension intecepted a terminal link but it timed out after ${TerminalLinkManager.LINK_INTERCEPT_THRESHOLD / 1000} seconds`); - r(false); - }, TerminalLinkManager.LINK_INTERCEPT_THRESHOLD); - let canceled = false; - const resolve = (handled: boolean) => { - if (!canceled) { - clearTimeout(timeoutId); - r(handled); - } - }; - this._onBeforeHandleLink.fire({ link, resolve }); - }); - } - protected get _localLinkRegex(): RegExp { if (!this._processManager) { throw new Error('Process manager is required'); @@ -369,7 +324,6 @@ export class TerminalLinkManager extends DisposableStore { * @param link Url link which may contain line and column number. */ public extractLineColumnInfo(link: string): LineColumnInfo { - const matches: string[] | null = this._localLinkRegex.exec(link); const lineColumnInfo: LineColumnInfo = { lineNumber: 1, diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider.ts index 2e47dfe72ef..615857b82ac 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Terminal, IViewportRange, IBufferLine } from 'xterm'; +import type { Terminal, IViewportRange, IBufferLine } from 'xterm'; import { ILinkComputerTarget, LinkComputer } from 'vs/editor/common/modes/linkComputer'; import { getXtermLineContent, convertLinkRangeToBuffer } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers'; import { TerminalLink, OPEN_FILE_LABEL } from 'vs/workbench/contrib/terminal/browser/links/terminalLink'; diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts index 287afdf9e1a..ada3c0be88d 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Terminal, IViewportRange, IBufferLine } from 'xterm'; +import type { Terminal, IViewportRange, IBufferLine } from 'xterm'; import { getXtermLineContent, convertLinkRangeToBuffer } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers'; import { OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts index 5b501192c49..3947f9a1556 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Terminal, IViewportRange } from 'xterm'; +import type { Terminal, IViewportRange } from 'xterm'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITerminalConfiguration, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink'; diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 43de91e60fb..8c394244f15 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -196,3 +196,7 @@ padding: 0 22px 0 6px; } +/* HACK: Can remove when fixed upstream https://github.com/xtermjs/xterm.js/issues/3058 */ +.xterm-helper-textarea { + border: 0px; +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index a2915b88d67..9a9fa34d16e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -21,7 +21,7 @@ import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/wor import { Extensions as ViewContainerExtensions, IViewContainersRegistry, ViewContainerLocation, IViewsRegistry } from 'vs/workbench/common/views'; import { registerTerminalActions, ClearTerminalAction, CopyTerminalSelectionAction, CreateNewTerminalAction, KillTerminalAction, SelectAllTerminalAction, SelectDefaultShellWindowsTerminalAction, SplitInActiveWorkspaceTerminalAction, SplitTerminalAction, TerminalPasteAction, ToggleTerminalAction, terminalSendSequenceCommand } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; -import { KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE_KEY, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_VIEW_ID, TERMINAL_ACTION_CATEGORY, TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminal'; +import { KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE_KEY, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_VIEW_ID, TERMINAL_ACTION_CATEGORY, TERMINAL_COMMAND_ID, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED } from 'vs/workbench/contrib/terminal/common/terminal'; import { registerColors } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { setupTerminalCommands } from 'vs/workbench/contrib/terminal/browser/terminalCommands'; import { setupTerminalMenu } from 'vs/workbench/contrib/terminal/common/terminalMenu'; @@ -84,7 +84,7 @@ Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews const actionRegistry = Registry.as(ActionExtensions.WorkbenchActions); registerTerminalActions(); const category = TERMINAL_ACTION_CATEGORY; -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(KillTerminalAction), 'Terminal: Kill the Active Terminal Instance', category); +actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(KillTerminalAction), 'Terminal: Kill the Active Terminal Instance', category, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(CreateNewTerminalAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_BACKTICK, mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.US_BACKTICK } @@ -97,7 +97,7 @@ actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(SelectAllTermin // behavior anyway when handed to xterm.js, having this handled by VS Code // makes it easier for users to see how it works though. mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_A } -}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Select All', category); +}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Select All', category, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleTerminalAction, { primary: KeyMod.CtrlCmd | KeyCode.US_BACKTICK, mac: { primary: KeyMod.WinCtrl | KeyCode.US_BACKTICK } @@ -107,16 +107,16 @@ actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleTerminalA actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ClearTerminalAction, { primary: 0, mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_K } -}, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KeybindingWeight.WorkbenchContrib + 1), 'Terminal: Clear', category); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(SelectDefaultShellWindowsTerminalAction), 'Terminal: Select Default Shell', category); +}, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KeybindingWeight.WorkbenchContrib + 1), 'Terminal: Clear', category, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED); +actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(SelectDefaultShellWindowsTerminalAction), 'Terminal: Select Default Shell', category, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(SplitTerminalAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_5, mac: { primary: KeyMod.CtrlCmd | KeyCode.US_BACKSLASH, secondary: [KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_5] } -}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Split Terminal', category); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(SplitInActiveWorkspaceTerminalAction), 'Terminal: Split Terminal (In Active Workspace)', category); +}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Split Terminal', category, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED); +actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(SplitInActiveWorkspaceTerminalAction), 'Terminal: Split Terminal (In Active Workspace)', category, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED); // Commands might be affected by Web restrictons if (BrowserFeatures.clipboard.writeText) { @@ -124,7 +124,7 @@ if (BrowserFeatures.clipboard.writeText) { primary: KeyMod.CtrlCmd | KeyCode.KEY_C, win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_C, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C] }, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C } - }, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, KEYBINDING_CONTEXT_TERMINAL_FOCUS)), 'Terminal: Copy Selection', category); + }, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, KEYBINDING_CONTEXT_TERMINAL_FOCUS)), 'Terminal: Copy Selection', category, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED); } function registerSendSequenceKeybinding(text: string, rule: { when?: ContextKeyExpression } & IKeybindings): void { @@ -149,7 +149,7 @@ if (BrowserFeatures.clipboard.readText) { primary: KeyMod.CtrlCmd | KeyCode.KEY_V, win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_V, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_V] }, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_V } - }, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Paste into Active Terminal', category); + }, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Paste into Active Terminal', category, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED); // An extra Windows-only ctrl+v keybinding is used for pwsh that sends ctrl+v directly to the // shell, this gets handled by PSReadLine which properly handles multi-line pastes. This is // disabled in accessibility mode as PowerShell does not run PSReadLine when it detects a screen diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 39098571234..84631cdac69 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Terminal as XTermTerminal } from 'xterm'; -import { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; -import { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11'; -import { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; -import { IWindowsShellHelper, ITerminalConfigHelper, ITerminalChildProcess, IShellLaunchConfig, IDefaultShellAndArgsRequest, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, ITerminalProcessExtHostProxy, ICommandTracker, INavigationMode, TitleEventSource, ITerminalDimensions, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; +import type { Terminal as XTermTerminal } from 'xterm'; +import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; +import type { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11'; +import type { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; +import { IWindowsShellHelper, ITerminalConfigHelper, ITerminalChildProcess, IShellLaunchConfig, IDefaultShellAndArgsRequest, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, ITerminalProcessExtHostProxy, ICommandTracker, INavigationMode, TitleEventSource, ITerminalDimensions, ITerminalLaunchError, ITerminalNativeWindowsDelegate, LinuxDistro } from 'vs/workbench/contrib/terminal/common/terminal'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProcessEnvironment, Platform } from 'vs/base/common/platform'; import { Event } from 'vs/base/common/event'; @@ -76,6 +76,7 @@ export interface ITerminalService { configHelper: ITerminalConfigHelper; terminalInstances: ITerminalInstance[]; terminalTabs: ITerminalTab[]; + isProcessSupportRegistered: boolean; onActiveTabChanged: Event; onTabDisposed: Event; @@ -90,6 +91,7 @@ export interface ITerminalService { onInstanceTitleChanged: Event; onActiveInstanceChanged: Event; onRequestAvailableShells: Event; + onDidRegisterProcessSupport: Event; /** * Creates a terminal. @@ -136,14 +138,7 @@ export interface ITerminalService { findNext(): void; findPrevious(): void; - /** - * Link handlers can be registered here to allow intercepting links clicked in the terminal. - * When a link is clicked, the link will be considered handled when the first interceptor - * resolves with true. It will be considered not handled when _all_ link handlers resolve with - * false, or 3 seconds have elapsed. - */ - addLinkHandler(key: string, callback: TerminalLinkHandlerCallback): IDisposable; - + registerProcessSupport(isSupported: boolean): void; /** * Registers a link provider that enables integrators to add links to the terminal. * @param linkProvider When registered, the link provider is asked whenever a cell is hovered @@ -157,6 +152,12 @@ export interface ITerminalService { setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void; manageWorkspaceShellPermissions(): void; + /** + * Injects native Windows functionality into the service. + */ + setNativeWindowsDelegate(delegate: ITerminalNativeWindowsDelegate): void; + setLinuxDistro(linuxDistro: LinuxDistro): void; + /** * Takes a path and returns the properly escaped path to send to the terminal. * On Windows, this included trying to prepare the path for WSL if needed. @@ -215,8 +216,6 @@ export enum WindowsShellType { } export type TerminalShellType = WindowsShellType | undefined; -export const LINK_INTERCEPT_THRESHOLD = 3000; - export interface ITerminalBeforeHandleLinkEvent { terminal?: ITerminalInstance; /** The text of the link */ @@ -225,8 +224,6 @@ export interface ITerminalBeforeHandleLinkEvent { resolve(wasHandled: boolean): void; } -export type TerminalLinkHandlerCallback = (e: ITerminalBeforeHandleLinkEvent) => Promise; - export interface ITerminalInstance { /** * The ID of the terminal instance, this is an arbitrary number only used to identify the @@ -289,15 +286,18 @@ export interface ITerminalInstance { */ onExit: Event; - /** - * Attach a listener to intercept and handle link clicks in the terminal. - */ - onBeforeHandleLink: Event; - readonly exitCode: number | undefined; readonly areLinksReady: boolean; + /** + * Returns an array of data events that have fired within the first 10 seconds. If this is + * called 10 seconds after the terminal has existed the result will be undefined. This is useful + * when objects that depend on the data events have delayed initialization, like extension + * hosts. + */ + readonly initialDataEvents: string[] | undefined; + /** A promise that resolves when the terminal's pty/process have been created. */ processReady: Promise; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index dd4b2216ff2..b532ed7aae5 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -6,7 +6,7 @@ import { Action, IAction } from 'vs/base/common/actions'; import { EndOfLinePreference } from 'vs/editor/common/model'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { TERMINAL_VIEW_ID, ITerminalConfigHelper, TitleEventSource, TERMINAL_COMMAND_ID, KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED, TERMINAL_ACTION_CATEGORY, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, KEYBINDING_CONTEXT_TERMINAL_FIND_NOT_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TERMINAL_VIEW_ID, ITerminalConfigHelper, TitleEventSource, TERMINAL_COMMAND_ID, KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED, TERMINAL_ACTION_CATEGORY, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, KEYBINDING_CONTEXT_TERMINAL_FIND_NOT_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED } from 'vs/workbench/contrib/terminal/common/terminal'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { attachSelectBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -367,7 +367,7 @@ export class SwitchTerminalActionViewItem extends SelectActionViewItem { this._register(_terminalService.onActiveTabChanged(this._updateItems, this)); this._register(_terminalService.onInstanceTitleChanged(this._updateItems, this)); this._register(_terminalService.onTabDisposed(this._updateItems, this)); - this._register(attachSelectBoxStyler(this.selectBox, _themeService)); + this._register(attachSelectBoxStyler(this.selectBox, this._themeService)); } render(container: HTMLElement): void { @@ -466,7 +466,8 @@ export function registerTerminalActions() { }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -491,7 +492,8 @@ export function registerTerminalActions() { }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -512,7 +514,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.LeftArrow }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -531,7 +534,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.RightArrow }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -549,7 +553,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.UpArrow }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -567,7 +572,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.DownArrow }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -580,7 +586,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.FOCUS, title: { value: localize('workbench.action.terminal.focus', "Focus Terminal"), original: 'Focus Terminal' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -599,7 +606,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.FOCUS_NEXT, title: { value: localize('workbench.action.terminal.focusNext', "Focus Next Terminal"), original: 'Focus Next Terminal' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -614,7 +622,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.FOCUS_PREVIOUS, title: { value: localize('workbench.action.terminal.focusPrevious', "Focus Previous Terminal"), original: 'Focus Previous Terminal' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -629,7 +638,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.RUN_SELECTED_TEXT, title: { value: localize('workbench.action.terminal.runSelectedText', "Run Selected Text In Active Terminal"), original: 'Run Selected Text In Active Terminal' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -659,7 +669,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.RUN_ACTIVE_FILE, title: { value: localize('workbench.action.terminal.runActiveFile', "Run Active File In Active Terminal"), original: 'Run Active File In Active Terminal' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -699,7 +710,8 @@ export function registerTerminalActions() { linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -718,7 +730,8 @@ export function registerTerminalActions() { mac: { primary: KeyCode.PageDown }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -737,7 +750,8 @@ export function registerTerminalActions() { linux: { primary: KeyMod.Shift | KeyCode.End }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -756,7 +770,8 @@ export function registerTerminalActions() { linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -775,7 +790,8 @@ export function registerTerminalActions() { mac: { primary: KeyCode.PageUp }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -794,7 +810,8 @@ export function registerTerminalActions() { linux: { primary: KeyMod.Shift | KeyCode.Home }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -812,7 +829,8 @@ export function registerTerminalActions() { primary: KeyCode.Escape, when: ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, CONTEXT_ACCESSIBILITY_MODE_ENABLED), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -833,7 +851,8 @@ export function registerTerminalActions() { ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, CONTEXT_ACCESSIBILITY_MODE_ENABLED) ), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -854,7 +873,8 @@ export function registerTerminalActions() { ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, CONTEXT_ACCESSIBILITY_MODE_ENABLED) ), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -872,7 +892,8 @@ export function registerTerminalActions() { primary: KeyCode.Escape, when: ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, KEYBINDING_CONTEXT_TERMINAL_FIND_NOT_VISIBLE), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -888,7 +909,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.MANAGE_WORKSPACE_SHELL_PERMISSIONS, title: { value: localize('workbench.action.terminal.manageWorkspaceShellPermissions', "Manage Workspace Shell Permissions"), original: 'Manage Workspace Shell Permissions' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -901,7 +923,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.RENAME, title: { value: localize('workbench.action.terminal.rename', "Rename"), original: 'Rename' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -927,7 +950,8 @@ export function registerTerminalActions() { primary: KeyMod.CtrlCmd | KeyCode.KEY_F, when: ContextKeyExpr.or(KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_FOCUS), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -946,7 +970,8 @@ export function registerTerminalActions() { secondary: [KeyMod.Shift | KeyCode.Escape], when: ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -959,7 +984,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.QUICK_OPEN_TERM, title: { value: localize('quickAccessTerminal', "Switch Active Terminal"), original: 'Switch Active Terminal' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -977,7 +1003,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyCode.UpArrow }, when: ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -998,7 +1025,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyCode.DownArrow }, when: ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1019,7 +1047,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1040,7 +1069,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1056,7 +1086,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.SELECT_TO_PREVIOUS_LINE, title: { value: localize('workbench.action.terminal.selectToPreviousLine', "Select To Previous Line"), original: 'Select To Previous Line' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1072,7 +1103,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.SELECT_TO_NEXT_LINE, title: { value: localize('workbench.action.terminal.selectToNextLine', "Select To Next Line"), original: 'Select To Next Line' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1088,7 +1120,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.TOGGLE_ESCAPE_SEQUENCE_LOGGING, title: { value: localize('workbench.action.terminal.toggleEscapeSequenceLogging', "Toggle Escape Sequence Logging"), original: 'Toggle Escape Sequence Logging' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1114,7 +1147,8 @@ export function registerTerminalActions() { }, } }] - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor, args?: { text?: string }) { @@ -1143,7 +1177,8 @@ export function registerTerminalActions() { }, } }] - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor, args?: { cwd?: string }) { @@ -1179,7 +1214,8 @@ export function registerTerminalActions() { } } }] - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor, args?: { name?: string }) { @@ -1203,7 +1239,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_R }, when: ContextKeyExpr.or(KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1224,6 +1261,7 @@ export function registerTerminalActions() { when: ContextKeyExpr.or(KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED), weight: KeybindingWeight.WorkbenchContrib }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1243,7 +1281,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C }, when: ContextKeyExpr.or(KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1270,7 +1309,8 @@ export function registerTerminalActions() { when: KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED, weight: KeybindingWeight.WorkbenchContrib } - ] + ], + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1296,7 +1336,8 @@ export function registerTerminalActions() { when: KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED, weight: KeybindingWeight.WorkbenchContrib } - ] + ], + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1309,7 +1350,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.RELAUNCH, title: { value: localize('workbench.action.terminal.relaunch', "Relaunch Active Terminal"), original: 'Relaunch Active Terminal' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1322,7 +1364,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.SHOW_ENVIRONMENT_INFORMATION, title: { value: localize('workbench.action.terminal.showEnvironmentInformation', "Show Environment Information"), original: 'Show Environment Information' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts index 82f060e64cc..e39451c4b69 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts @@ -35,13 +35,13 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { private _charMeasureElement: HTMLElement | undefined; private _lastFontMeasurement: ITerminalFont | undefined; + private _linuxDistro: LinuxDistro = LinuxDistro.Unknown; public config!: ITerminalConfiguration; private readonly _onWorkspacePermissionsChanged = new Emitter(); public get onWorkspacePermissionsChanged(): Event { return this._onWorkspacePermissionsChanged.event; } public constructor( - private readonly _linuxDistro: LinuxDistro, @IConfigurationService private readonly _configurationService: IConfigurationService, @IExtensionManagementService private readonly _extensionManagementService: IExtensionManagementService, @INotificationService private readonly _notificationService: INotificationService, @@ -62,6 +62,10 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { storageKeysSyncRegistryService.registerStorageKey({ key: 'terminalConfigHelper/launchRecommendationsIgnore', version: 1 }); } + public setLinuxDistro(linuxDistro: LinuxDistro) { + this._linuxDistro = linuxDistro; + } + private _updateConfig(): void { this.config = this._configurationService.getValue(TERMINAL_CONFIG_SECTION); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 6ca70138147..f80982e132a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -30,11 +30,11 @@ import { ansiColorIdentifiers, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_BACKGR import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { ITerminalInstanceService, ITerminalInstance, TerminalShellType, WindowsShellType, ITerminalBeforeHandleLinkEvent, ITerminalExternalLinkProvider } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalInstanceService, ITerminalInstance, TerminalShellType, WindowsShellType, ITerminalExternalLinkProvider } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalProcessManager } from 'vs/workbench/contrib/terminal/browser/terminalProcessManager'; -import { Terminal as XTermTerminal, IBuffer, ITerminalAddon } from 'xterm'; -import { SearchAddon, ISearchOptions } from 'xterm-addon-search'; -import { Unicode11Addon } from 'xterm-addon-unicode11'; +import type { Terminal as XTermTerminal, IBuffer, ITerminalAddon } from 'xterm'; +import type { SearchAddon, ISearchOptions } from 'xterm-addon-search'; +import type { Unicode11Addon } from 'xterm-addon-unicode11'; import { CommandTrackerAddon } from 'vs/workbench/contrib/terminal/browser/addons/commandTrackerAddon'; import { NavigationModeAddon } from 'vs/workbench/contrib/terminal/browser/addons/navigationModeAddon'; import { XTermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; @@ -98,6 +98,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _titleReadyPromise: Promise; private _titleReadyComplete: ((title: string) => any) | undefined; private _areLinksReady: boolean = false; + private _initialDataEvents: string[] | undefined = []; private _messageTitleDisposable: IDisposable | undefined; @@ -131,6 +132,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // TODO: Should this be an event as it can fire twice? public get processReady(): Promise { return this._processManager.ptyProcessReady; } public get areLinksReady(): boolean { return this._areLinksReady; } + public get initialDataEvents(): string[] | undefined { return this._initialDataEvents; } public get exitCode(): number | undefined { return this._exitCode; } public get title(): string { return this._title; } public get hadFocusOnExit(): boolean { return this._hadFocusOnExit; } @@ -164,8 +166,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { public get onMaximumDimensionsChanged(): Event { return this._onMaximumDimensionsChanged.event; } private readonly _onFocus = new Emitter(); public get onFocus(): Event { return this._onFocus.event; } - private readonly _onBeforeHandleLink = new Emitter(); - public get onBeforeHandleLink(): Event { return this._onBeforeHandleLink.event; } public constructor( private readonly _terminalFocusContextKey: IContextKey, @@ -233,6 +233,20 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this.updateAccessibilitySupport(); } })); + + // Clear out initial data events after 10 seconds, hopefully extension hosts are up and + // running at that point. + let initialDataEventsTimeout: number | undefined = window.setTimeout(() => { + initialDataEventsTimeout = undefined; + this._initialDataEvents = undefined; + }, 10000); + this._register({ + dispose: () => { + if (initialDataEventsTimeout) { + window.clearTimeout(initialDataEventsTimeout); + } + } + }); } public addDisposable(disposable: IDisposable): void { @@ -419,10 +433,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { }); } this._linkManager = this._instantiationService.createInstance(TerminalLinkManager, xterm, this._processManager!); - this._linkManager.onBeforeHandleLink(e => { - e.terminal = this; - this._onBeforeHandleLink.fire(e); - }); this._areLinksReady = true; this._onLinksReady.fire(this); }); @@ -868,11 +878,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { protected _createProcessManager(): void { this._processManager = this._instantiationService.createInstance(TerminalProcessManager, this._id, this._configHelper); - this._processManager.onProcessReady(() => { - this._onProcessIdReady.fire(this); - }); + this._processManager.onProcessReady(() => this._onProcessIdReady.fire(this)); this._processManager.onProcessExit(exitCode => this._onProcessExit(exitCode)); - this._processManager.onProcessData(data => this._onData.fire(data)); + this._processManager.onProcessData(data => { + this._initialDataEvents?.push(data); + this._onData.fire(data); + }); this._processManager.onProcessOverrideDimensions(e => this.setDimensions(e)); this._processManager.onProcessResolvedShellLaunchConfig(e => this._setResolvedShellLaunchConfig(e)); this._processManager.onEnvironmentVariableInfoChanged(e => this._onEnvironmentVariableInfoChanged(e)); @@ -1117,9 +1128,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (!reset) { // HACK: Force initialText to be non-falsy for reused terminals such that the - // conptyInheritCursor flag is passed to the node-pty, this flag can cause a Window to hang - // in Windows 10 1903 so we only want to use it when something is definitely written to the - // terminal. + // conptyInheritCursor flag is passed to the node-pty, this flag can cause a Window to stop + // responding in Windows 10 1903 so we only want to use it when something is definitely written + // to the terminal. shell.initialText = ' '; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts index 52b03f24ccb..daefe15f7ef 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts @@ -5,10 +5,10 @@ import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IWindowsShellHelper, ITerminalChildProcess, IDefaultShellAndArgsRequest } from 'vs/workbench/contrib/terminal/common/terminal'; -import { Terminal as XTermTerminal } from 'xterm'; -import { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; -import { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11'; -import { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; +import type { Terminal as XTermTerminal } from 'xterm'; +import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; +import type { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11'; +import type { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { Emitter, Event } from 'vs/base/common/event'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index cb0c8c2fca0..de6a1f38ff7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -4,17 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { TERMINAL_VIEW_ID, IShellLaunchConfig, ITerminalConfigHelper, ITerminalNativeService, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, ITerminalProcessExtHostProxy, IShellDefinition, LinuxDistro, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TERMINAL_VIEW_ID, IShellLaunchConfig, ITerminalConfigHelper, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, ITerminalProcessExtHostProxy, IShellDefinition, LinuxDistro, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, ITerminalLaunchError, ITerminalNativeWindowsDelegate } from 'vs/workbench/contrib/terminal/common/terminal'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { TerminalTab } from 'vs/workbench/contrib/terminal/browser/terminalTab'; -import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance'; -import { ITerminalService, ITerminalInstance, ITerminalTab, TerminalShellType, WindowsShellType, TerminalLinkHandlerCallback, LINK_INTERCEPT_THRESHOLD, ITerminalExternalLinkProvider } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalService, ITerminalInstance, ITerminalTab, TerminalShellType, WindowsShellType, ITerminalExternalLinkProvider } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { IQuickInputService, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput'; @@ -23,12 +23,13 @@ import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { escapeNonWindowsPath } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; -import { isWindows, isMacintosh, OperatingSystem } from 'vs/base/common/platform'; +import { isWindows, isMacintosh, OperatingSystem, isWeb } from 'vs/base/common/platform'; import { basename } from 'vs/base/common/path'; import { find } from 'vs/base/common/arrays'; import { timeout } from 'vs/base/common/async'; import { IViewsService, ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; interface IExtHostReadyEntry { promise: Promise; @@ -50,16 +51,18 @@ export class TerminalService implements ITerminalService { private _findState: FindReplaceState; private _extHostsReady: { [authority: string]: IExtHostReadyEntry | undefined } = {}; private _activeTabIndex: number; - private _linkHandlers: { [key: string]: TerminalLinkHandlerCallback } = {}; private _linkProviders: Set = new Set(); private _linkProviderDisposables: Map = new Map(); + private _processSupportContextKey: IContextKey; public get activeTabIndex(): number { return this._activeTabIndex; } public get terminalInstances(): ITerminalInstance[] { return this._terminalInstances; } public get terminalTabs(): ITerminalTab[] { return this._terminalTabs; } + public get isProcessSupportRegistered(): boolean { return !!this._processSupportContextKey.get(); } private _configHelper: TerminalConfigHelper; private _terminalContainer: HTMLElement | undefined; + private _nativeWindowsDelegate: ITerminalNativeWindowsDelegate | undefined; public get configHelper(): ITerminalConfigHelper { return this._configHelper; } @@ -91,8 +94,8 @@ export class TerminalService implements ITerminalService { public get onTabDisposed(): Event { return this._onTabDisposed.event; } private readonly _onRequestAvailableShells = new Emitter(); public get onRequestAvailableShells(): Event { return this._onRequestAvailableShells.event; } - - private readonly _terminalNativeService: ITerminalNativeService | undefined; + private readonly _onDidRegisterProcessSupport = new Emitter(); + public get onDidRegisterProcessSupport(): Event { return this._onDidRegisterProcessSupport.event; } constructor( @IContextKeyService private _contextKeyService: IContextKeyService, @@ -106,33 +109,17 @@ export class TerminalService implements ITerminalService { @IConfigurationService private _configurationService: IConfigurationService, @IViewsService private _viewsService: IViewsService, @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService, - // HACK: Ideally TerminalNativeService would depend on TerminalService and inject the - // additional native functionality into it. - @optional(ITerminalNativeService) terminalNativeService: ITerminalNativeService + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { - // @optional could give undefined and properly typing it breaks service registration - this._terminalNativeService = terminalNativeService as ITerminalNativeService | undefined; - this._activeTabIndex = 0; this._isShuttingDown = false; this._findState = new FindReplaceState(); lifecycleService.onBeforeShutdown(async event => event.veto(this._onBeforeShutdown())); lifecycleService.onShutdown(() => this._onShutdown()); - if (this._terminalNativeService) { - this._terminalNativeService.onRequestFocusActiveInstance(() => { - if (this.terminalInstances.length > 0) { - const terminal = this.getActiveInstance(); - if (terminal) { - terminal.focus(); - } - } - }); - this._terminalNativeService.onOsResume(() => this._onOsResume()); - } this._terminalFocusContextKey = KEYBINDING_CONTEXT_TERMINAL_FOCUS.bindTo(this._contextKeyService); this._terminalShellTypeContextKey = KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE.bindTo(this._contextKeyService); this._findWidgetVisible = KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE.bindTo(this._contextKeyService); - this._configHelper = this._instantiationService.createInstance(TerminalConfigHelper, this._terminalNativeService?.linuxDistro || LinuxDistro.Unknown); + this._configHelper = this._instantiationService.createInstance(TerminalConfigHelper); this.onTabDisposed(tab => this._removeTab(tab)); this.onActiveTabChanged(() => { const instance = this.getActiveInstance(); @@ -140,16 +127,24 @@ export class TerminalService implements ITerminalService { }); this.onInstanceLinksReady(instance => this._setInstanceLinkProviders(instance)); - this._handleContextKeys(); + this._handleInstanceContextKeys(); + this._processSupportContextKey = KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED.bindTo(this._contextKeyService); + this._processSupportContextKey.set(!isWeb || this._remoteAgentService.getConnection() !== null); } - private _handleContextKeys(): void { - const terminalIsOpenContext = KEYBINDING_CONTEXT_TERMINAL_IS_OPEN.bindTo(this._contextKeyService); + public setNativeWindowsDelegate(delegate: ITerminalNativeWindowsDelegate): void { + this._nativeWindowsDelegate = delegate; + } + public setLinuxDistro(linuxDistro: LinuxDistro): void { + this._configHelper.setLinuxDistro(linuxDistro); + } + + private _handleInstanceContextKeys(): void { + const terminalIsOpenContext = KEYBINDING_CONTEXT_TERMINAL_IS_OPEN.bindTo(this._contextKeyService); const updateTerminalContextKeys = () => { terminalIsOpenContext.set(this.terminalInstances.length > 0); }; - this.onInstancesChanged(() => updateTerminalContextKeys()); } @@ -226,14 +221,6 @@ export class TerminalService implements ITerminalService { this.terminalInstances.forEach(instance => instance.dispose(true)); } - private _onOsResume(): void { - const activeTab = this.getActiveTab(); - if (!activeTab) { - return; - } - activeTab.terminalInstances.forEach(instance => instance.forceRedraw()); - } - public getTabLabels(): string[] { return this._terminalTabs.filter(tab => tab.terminalInstances.length > 0).map((tab, index) => `${index + 1}: ${tab.title ? tab.title : ''}`); } @@ -428,50 +415,14 @@ export class TerminalService implements ITerminalService { instance.addDisposable(instance.onDimensionsChanged(() => this._onInstanceDimensionsChanged.fire(instance))); instance.addDisposable(instance.onMaximumDimensionsChanged(() => this._onInstanceMaximumDimensionsChanged.fire(instance))); instance.addDisposable(instance.onFocus(this._onActiveInstanceChanged.fire, this._onActiveInstanceChanged)); - instance.addDisposable(instance.onBeforeHandleLink(async e => { - // No link handlers have been registered - const keys = Object.keys(this._linkHandlers); - if (keys.length === 0) { - e.resolve(false); - return; - } - - // Fire each link interceptor and wait for either a true, all false or the cancel time - let resolved = false; - const promises: Promise[] = []; - const timeout = setTimeout(() => { - resolved = true; - e.resolve(false); - }, LINK_INTERCEPT_THRESHOLD); - for (let i = 0; i < keys.length; i++) { - const p = this._linkHandlers[keys[i]](e); - p.then(handled => { - if (!resolved && handled) { - resolved = true; - clearTimeout(timeout); - e.resolve(true); - } - }); - promises.push(p); - } - await Promise.all(promises); - if (!resolved) { - resolved = true; - clearTimeout(timeout); - e.resolve(false); - } - })); } - public addLinkHandler(key: string, callback: TerminalLinkHandlerCallback): IDisposable { - this._linkHandlers[key] = callback; - return { - dispose: () => { - if (this._linkHandlers[key] === callback) { - delete this._linkHandlers[key]; - } - } - }; + public registerProcessSupport(isSupported: boolean): void { + if (!isSupported) { + return; + } + this._processSupportContextKey.set(isSupported); + this._onDidRegisterProcessSupport.fire(); } public registerLinkProvider(linkProvider: ITerminalExternalLinkProvider): IDisposable { @@ -588,8 +539,8 @@ export class TerminalService implements ITerminalService { return; } else if (shellType === WindowsShellType.Wsl) { - if (this._terminalNativeService && this._terminalNativeService.getWindowsBuildNumber() >= 17063) { - c(this._terminalNativeService.getWslPath(originalPath)); + if (this._nativeWindowsDelegate && this._nativeWindowsDelegate.getWindowsBuildNumber() >= 17063) { + c(this._nativeWindowsDelegate.getWslPath(originalPath)); } else { c(originalPath.replace(/\\/g, '/')); } @@ -603,9 +554,9 @@ export class TerminalService implements ITerminalService { } } else { const lowerExecutable = executable.toLowerCase(); - if (this._terminalNativeService && this._terminalNativeService.getWindowsBuildNumber() >= 17063 && + if (this._nativeWindowsDelegate && this._nativeWindowsDelegate.getWindowsBuildNumber() >= 17063 && (lowerExecutable.indexOf('wsl') !== -1 || (lowerExecutable.indexOf('bash.exe') !== -1 && lowerExecutable.toLowerCase().indexOf('git') === -1))) { - c(this._terminalNativeService.getWslPath(originalPath)); + c(this._nativeWindowsDelegate.getWslPath(originalPath)); return; } else if (hasSpace) { c('"' + originalPath + '"'); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 55d0d5e1865..ae87e42d990 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -19,7 +19,6 @@ import { URI } from 'vs/base/common/uri'; import { TERMINAL_BACKGROUND_COLOR, TERMINAL_BORDER_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { DataTransfers } from 'vs/base/browser/dnd'; import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; -import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; @@ -56,14 +55,25 @@ export class TerminalViewPane extends ViewPane { @IThemeService protected readonly themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, @INotificationService private readonly _notificationService: INotificationService, - @IStorageService storageService: IStorageService, @IOpenerService openerService: IOpenerService, ) { super(options, keybindingService, _contextMenuService, configurationService, contextKeyService, viewDescriptorService, _instantiationService, openerService, themeService, telemetryService); + this._terminalService.onDidRegisterProcessSupport(() => { + if (this._actions) { + for (const action of this._actions) { + action.enabled = true; + } + } + this._onDidChangeViewWelcomeState.fire(); + }); } protected renderBody(container: HTMLElement): void { super.renderBody(container); + if (this.shouldShowWelcome()) { + return; + } + this._parentDomElement = container; dom.addClass(this._parentDomElement, 'integrated-terminal'); this._fontStyleElement = document.createElement('style'); @@ -120,6 +130,10 @@ export class TerminalViewPane extends ViewPane { protected layoutBody(height: number, width: number): void { super.layoutBody(height, width); + if (this.shouldShowWelcome()) { + return; + } + this._bodyDimensions.width = width; this._bodyDimensions.height = height; this._terminalService.terminalTabs.forEach(t => t.layout(width, height)); @@ -138,9 +152,12 @@ export class TerminalViewPane extends ViewPane { this._splitTerminalAction, this._instantiationService.createInstance(KillTerminalAction, KillTerminalAction.ID, KillTerminalAction.PANEL_LABEL) ]; - this._actions.forEach(a => { - this._register(a); - }); + for (const action of this._actions) { + if (!this._terminalService.isProcessSupportRegistered) { + action.enabled = false; + } + this._register(action); + } } return this._actions; } @@ -188,10 +205,7 @@ export class TerminalViewPane extends ViewPane { } public focus(): void { - const activeInstance = this._terminalService.getActiveInstance(); - if (activeInstance) { - activeInstance.focusWhenReady(true); - } + this._terminalService.getActiveInstance()?.focusWhenReady(true); } public focusFindWidget() { @@ -331,9 +345,11 @@ export class TerminalViewPane extends ViewPane { theme = this.themeService.getColorTheme(); } - if (this._findWidget) { - this._findWidget.updateTheme(theme); - } + this._findWidget?.updateTheme(theme); + } + + shouldShowWelcome(): boolean { + return !this._terminalService.isProcessSupportRegistered; } } diff --git a/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts b/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts index 121573ea0f5..7633a258cbe 100644 --- a/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts @@ -8,7 +8,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Widget } from 'vs/base/browser/ui/widget'; import { ITerminalWidget } from 'vs/workbench/contrib/terminal/browser/widgets/widgets'; import * as dom from 'vs/base/browser/dom'; -import { IViewportRange } from 'xterm'; +import type { IViewportRange } from 'xterm'; import { IHoverTarget, IHoverService } from 'vs/workbench/services/hover/browser/hover'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { editorHoverHighlight } from 'vs/platform/theme/common/colorRegistry'; diff --git a/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts b/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts index b5bf843ffc7..f84bacb1cf5 100644 --- a/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts @@ -23,6 +23,7 @@ export class TerminalWidgetManager implements IDisposable { } dispose(): void { + this.hideHovers(); if (this._container && this._container.parentElement) { this._container.parentElement.removeChild(this._container); this._container = undefined; diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index e0d0fb53029..9c4323340a0 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -7,13 +7,12 @@ import * as nls from 'vs/nls'; import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; import { OperatingSystem } from 'vs/base/common/platform'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { IExtensionPointDescriptor } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -export const TERMINAL_VIEW_ID = 'workbench.panel.terminal'; +export const TERMINAL_VIEW_ID = 'terminal'; /** A context key that is set when there is at least one opened integrated terminal. */ export const KEYBINDING_CONTEXT_TERMINAL_IS_OPEN = new RawContextKey('terminalIsOpen', false); @@ -47,6 +46,8 @@ export const KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED = new RawContextKey('terminalProcessSupported', false); + export const IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY = 'terminal.integrated.isWorkspaceShellAllowed'; export const NEVER_MEASURE_RENDER_TIME_STORAGE_KEY = 'terminal.integrated.neverMeasureRenderTime'; @@ -55,8 +56,6 @@ export const NEVER_MEASURE_RENDER_TIME_STORAGE_KEY = 'terminal.integrated.neverM // trying to create the corressponding object on the ext host. export const EXT_HOST_CREATION_DELAY = 100; -export const ITerminalNativeService = createDecorator('terminalNativeService'); - export const TerminalCursorStyle = { BLOCK: 'block', LINE: 'line', @@ -230,18 +229,17 @@ export interface IShellLaunchConfig { } /** - * Provides access to native or electron APIs to other terminal services. + * Provides access to native Windows calls that can be injected into non-native layers. */ -export interface ITerminalNativeService { - readonly _serviceBrand: undefined; - - readonly linuxDistro: LinuxDistro; - - readonly onRequestFocusActiveInstance: Event; - readonly onOsResume: Event; - +export interface ITerminalNativeWindowsDelegate { + /** + * Gets the Windows build number, eg. this would be `19041` for Windows 10 version 2004 + */ getWindowsBuildNumber(): number; - whenFileDeleted(path: URI): Promise; + /** + * Converts a regular Windows path into the WSL path equivalent, eg. `C:\` -> `/mnt/c` + * @param path The Windows path. + */ getWslPath(path: string): Promise; } diff --git a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts index 20c3551c8f7..2ad5db5eb26 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts @@ -93,7 +93,7 @@ export function shouldSetLangEnvVariable(env: platform.IProcessEnvironment, dete return true; } if (detectLocale === 'auto') { - return !env['LANG'] || env['LANG'].search(/\.UTF\-8$/) === -1; + return !env['LANG'] || (env['LANG'].search(/\.UTF\-8$/) === -1 && env['LANG'].search(/\.utf8$/) === -1); } return false; // 'off' } diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts index 1721bf226a6..263c15cb666 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts @@ -3,22 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalInstanceService } from 'vs/workbench/contrib/terminal/electron-browser/terminalInstanceService'; import { getSystemShell } from 'vs/workbench/contrib/terminal/node/terminal'; -import { TerminalNativeService } from 'vs/workbench/contrib/terminal/electron-browser/terminalNativeService'; -import { ITerminalNativeService } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TerminalNativeContribution } from 'vs/workbench/contrib/terminal/electron-browser/terminalNativeContribution'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; import { getTerminalShellConfiguration } from 'vs/workbench/contrib/terminal/common/terminalConfiguration'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; // This file contains additional desktop-only contributions on top of those in browser/ // Register services -registerSingleton(ITerminalNativeService, TerminalNativeService, true); registerSingleton(ITerminalInstanceService, TerminalInstanceService, true); +const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchRegistry.registerWorkbenchContribution(TerminalNativeContribution, LifecyclePhase.Ready); + // Register configurations const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration(getTerminalShellConfiguration(getSystemShell)); diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts index 2dbb6a0f2be..40dcda2cd6f 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts @@ -10,10 +10,10 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IProcessEnvironment, platform, Platform } from 'vs/base/common/platform'; import { TerminalProcess } from 'vs/workbench/contrib/terminal/node/terminalProcess'; import { getSystemShell } from 'vs/workbench/contrib/terminal/node/terminal'; -import { Terminal as XTermTerminal } from 'xterm'; -import { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; -import { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11'; -import { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; +import type { Terminal as XTermTerminal } from 'xterm'; +import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; +import type { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11'; +import type { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { getDefaultShell, getDefaultShellArgs } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage'; diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminalNativeService.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminalNativeContribution.ts similarity index 68% rename from src/vs/workbench/contrib/terminal/electron-browser/terminalNativeService.ts rename to src/vs/workbench/contrib/terminal/electron-browser/terminalNativeContribution.ts index 84030ff44a7..8f8919ebd0a 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/terminalNativeService.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/terminalNativeContribution.ts @@ -4,41 +4,40 @@ *--------------------------------------------------------------------------------------------*/ import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; -import { IOpenFileRequest } from 'vs/platform/windows/common/windows'; -import { ITerminalNativeService, LinuxDistro } from 'vs/workbench/contrib/terminal/common/terminal'; +import { INativeOpenFileRequest } from 'vs/platform/windows/common/windows'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; import { getWindowsBuildNumber, linuxDistro } from 'vs/workbench/contrib/terminal/node/terminal'; import { escapeNonWindowsPath } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; import { execFile } from 'child_process'; -import { Emitter, Event } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { registerRemoteContributions } from 'vs/workbench/contrib/terminal/electron-browser/terminalRemote'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { Disposable } from 'vs/base/common/lifecycle'; -import { INativeOpenFileRequest } from 'vs/platform/windows/node/window'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -export class TerminalNativeService extends Disposable implements ITerminalNativeService { +export class TerminalNativeContribution extends Disposable implements IWorkbenchContribution { public _serviceBrand: undefined; - public get linuxDistro(): LinuxDistro { return linuxDistro; } - - private readonly _onRequestFocusActiveInstance = this._register(new Emitter()); - public get onRequestFocusActiveInstance(): Event { return this._onRequestFocusActiveInstance.event; } - private readonly _onOsResume = this._register(new Emitter()); - public get onOsResume(): Event { return this._onOsResume.event; } - constructor( @IFileService private readonly _fileService: IFileService, + @ITerminalService private readonly _terminalService: ITerminalService, @IInstantiationService readonly instantiationService: IInstantiationService, - @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IElectronService electronService: IElectronService + @IRemoteAgentService readonly remoteAgentService: IRemoteAgentService, + @IElectronService readonly electronService: IElectronService ) { super(); - ipcRenderer.on('vscode:openFiles', (event: unknown, request: IOpenFileRequest) => this._onOpenFileRequest(request)); - this._register(electronService.onOSResume(() => this._onOsResume.fire())); + ipcRenderer.on('vscode:openFiles', (_: unknown, request: INativeOpenFileRequest) => this._onOpenFileRequest(request)); + this._register(electronService.onOSResume(() => this._onOsResume())); + + this._terminalService.setLinuxDistro(linuxDistro); + this._terminalService.setNativeWindowsDelegate({ + getWslPath: this._getWslPath.bind(this), + getWindowsBuildNumber: this._getWindowsBuildNumber.bind(this) + }); const connection = remoteAgentService.getConnection(); if (connection && connection.remoteAuthority) { @@ -46,18 +45,28 @@ export class TerminalNativeService extends Disposable implements ITerminalNative } } + private _onOsResume(): void { + const activeTab = this._terminalService.getActiveTab(); + if (!activeTab) { + return; + } + activeTab.terminalInstances.forEach(instance => instance.forceRedraw()); + } + private async _onOpenFileRequest(request: INativeOpenFileRequest): Promise { // if the request to open files is coming in from the integrated terminal (identified though // the termProgram variable) and we are instructed to wait for editors close, wait for the // marker file to get deleted and then focus back to the integrated terminal. if (request.termProgram === 'vscode' && request.filesToWait) { const waitMarkerFileUri = URI.revive(request.filesToWait.waitMarkerFileUri); - await this.whenFileDeleted(waitMarkerFileUri); - this._onRequestFocusActiveInstance.fire(); + await this._whenFileDeleted(waitMarkerFileUri); + + // Focus active terminal + this._terminalService.getActiveInstance()?.focus(); } } - public whenFileDeleted(path: URI): Promise { + private _whenFileDeleted(path: URI): Promise { // Complete when wait marker file is deleted return new Promise(resolve => { let running = false; @@ -80,7 +89,7 @@ export class TerminalNativeService extends Disposable implements ITerminalNative * Converts a path to a path on WSL using the wslpath utility. * @param path The original path. */ - public getWslPath(path: string): Promise { + private _getWslPath(path: string): Promise { if (getWindowsBuildNumber() < 17063) { throw new Error('wslpath does not exist on Windows build < 17063'); } @@ -92,7 +101,7 @@ export class TerminalNativeService extends Disposable implements ITerminalNative }); } - public getWindowsBuildNumber(): number { + private _getWindowsBuildNumber(): number { return getWindowsBuildNumber(); } } diff --git a/src/vs/workbench/contrib/terminal/electron-browser/windowsShellHelper.ts b/src/vs/workbench/contrib/terminal/electron-browser/windowsShellHelper.ts index 37267ec9f43..495e9fccc90 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/windowsShellHelper.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/windowsShellHelper.ts @@ -6,8 +6,8 @@ import * as platform from 'vs/base/common/platform'; import { Emitter, Event } from 'vs/base/common/event'; import { IWindowsShellHelper } from 'vs/workbench/contrib/terminal/common/terminal'; -import { Terminal as XTermTerminal } from 'xterm'; -import * as WindowsProcessTreeType from 'windows-process-tree'; +import type { Terminal as XTermTerminal } from 'xterm'; +import type * as WindowsProcessTreeType from 'windows-process-tree'; import { Disposable } from 'vs/base/common/lifecycle'; import { timeout } from 'vs/base/common/async'; diff --git a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts index c59d62f463e..f3a9e3da81c 100644 --- a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts +++ b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts @@ -5,7 +5,7 @@ import * as path from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; -import * as pty from 'node-pty'; +import type * as pty from 'node-pty'; import * as fs from 'fs'; import { Event, Emitter } from 'vs/base/common/event'; import { getWindowsBuildNumber } from 'vs/workbench/contrib/terminal/node/terminal'; @@ -18,6 +18,13 @@ import { findExecutable } from 'vs/workbench/contrib/terminal/node/terminalEnvir import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; +// Writing large amounts of data can be corrupted for some reason, after looking into this is +// appears to be a race condition around writing to the FD which may be based on how powerful the +// hardware is. The workaround for this is to space out when large amounts of data is being written +// to the terminal. See https://github.com/microsoft/vscode/issues/38137 +const WRITE_MAX_CHUNK_SIZE = 50; +const WRITE_INTERVAL_MS = 5; + export class TerminalProcess extends Disposable implements ITerminalChildProcess { private _exitCode: number | undefined; private _exitMessage: string | undefined; @@ -27,6 +34,8 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private _processStartupComplete: Promise | undefined; private _isDisposed: boolean = false; private _titleInterval: NodeJS.Timer | null = null; + private _writeQueue: string[] = []; + private _writeTimeout: NodeJS.Timeout | undefined; private readonly _initialCwd: string; private readonly _ptyOptions: pty.IPtyForkOptions | pty.IWindowsPtyForkOptions; @@ -81,7 +90,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } try { - this.setupPtyProcess(this._shellLaunchConfig, this._ptyOptions); + await this.setupPtyProcess(this._shellLaunchConfig, this._ptyOptions); return undefined; } catch (err) { this._logService.trace('IPty#spawn native exception', err); @@ -127,10 +136,10 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess return undefined; } - private setupPtyProcess(shellLaunchConfig: IShellLaunchConfig, options: pty.IPtyForkOptions): void { + private async setupPtyProcess(shellLaunchConfig: IShellLaunchConfig, options: pty.IPtyForkOptions): Promise { const args = shellLaunchConfig.args || []; this._logService.trace('IPty#spawn', shellLaunchConfig.executable, args, options); - const ptyProcess = pty.spawn(shellLaunchConfig.executable!, args, options); + const ptyProcess = (await import('node-pty')).spawn(shellLaunchConfig.executable!, args, options); this._ptyProcess = ptyProcess; this._processStartupComplete = new Promise(c => { this.onProcessReady(() => c()); @@ -232,8 +241,37 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess if (this._isDisposed || !this._ptyProcess) { return; } + for (let i = 0; i <= Math.floor(data.length / WRITE_MAX_CHUNK_SIZE); i++) { + this._writeQueue.push(data.substr(i * WRITE_MAX_CHUNK_SIZE, WRITE_MAX_CHUNK_SIZE)); + } + this._startWrite(); + } + + private _startWrite(): void { + // Don't write if it's already queued of is there is nothing to write + if (this._writeTimeout !== undefined || this._writeQueue.length === 0) { + return; + } + + this._doWrite(); + + // Don't queue more writes if the queue is empty + if (this._writeQueue.length === 0) { + this._writeTimeout = undefined; + return; + } + + // Queue the next write + this._writeTimeout = setTimeout(() => { + this._writeTimeout = undefined; + this._startWrite(); + }, WRITE_INTERVAL_MS); + } + + private _doWrite(): void { + const data = this._writeQueue.shift()!; this._logService.trace('IPty#write', `${data.length} characters`); - this._ptyProcess.write(data); + this._ptyProcess!.write(data); } public resize(cols: number, rows: number): void { diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkManager.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkManager.test.ts deleted file mode 100644 index 2223e0d082a..00000000000 --- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkManager.test.ts +++ /dev/null @@ -1,163 +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 { OperatingSystem } from 'vs/base/common/platform'; -import { TerminalLinkManager, XtermLinkMatcherHandler } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; -import { Terminal as XtermTerminal } from 'xterm'; -import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { Event } from 'vs/base/common/event'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { TestPathService, TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; - -class TestTerminalLinkManager extends TerminalLinkManager { - public get localLinkRegex(): RegExp { - return this._localLinkRegex; - } - public preprocessPath(link: string): string | null { - return this._preprocessPath(link); - } - protected _isLinkActivationModifierDown(event: MouseEvent): boolean { - return true; - } - public wrapLinkHandler(handler: (link: string) => void): XtermLinkMatcherHandler { - TerminalLinkManager._LINK_INTERCEPT_THRESHOLD = 0; - return this._wrapLinkHandler((_, link) => handler(link)); - } -} - -class MockTerminalInstanceService implements ITerminalInstanceService { - onRequestDefaultShellAndArgs?: Event | undefined; - getDefaultShellAndArgs(): Promise<{ shell: string; args: string | string[] | undefined; }> { - throw new Error('Method not implemented.'); - } - declare readonly _serviceBrand: undefined; - getXtermConstructor(): Promise { - throw new Error('Method not implemented.'); - } - getXtermSearchConstructor(): Promise { - throw new Error('Method not implemented.'); - } - getXtermUnicode11Constructor(): Promise { - throw new Error('Method not implemented.'); - } - getXtermWebglConstructor(): Promise { - throw new Error('Method not implemented.'); - } - createWindowsShellHelper(): any { - throw new Error('Method not implemented.'); - } - createTerminalProcess(): any { - throw new Error('Method not implemented.'); - } - getMainProcessParentEnv(): any { - throw new Error('Method not implemented.'); - } -} - -suite('Workbench - TerminalLinkManager', () => { - let instantiationService: TestInstantiationService; - - setup(async () => { - const configurationService = new TestConfigurationService(); - await configurationService.setUserConfiguration('terminal', { integrated: { enableFileLinks: true } }); - - instantiationService = new TestInstantiationService(); - instantiationService.stub(IEnvironmentService, TestEnvironmentService); - instantiationService.stub(IPathService, new TestPathService()); - instantiationService.stub(ITerminalInstanceService, new MockTerminalInstanceService()); - instantiationService.stub(IConfigurationService, configurationService); - }); - - suite('preprocessPath', () => { - test('Windows', () => { - const linkHandler: TestTerminalLinkManager = instantiationService.createInstance(TestTerminalLinkManager, new XtermTerminal() as any, { - os: OperatingSystem.Windows, - userHome: 'C:\\Users\\Me' - } as any); - linkHandler.processCwd = 'C:\\base'; - - assert.equal(linkHandler.preprocessPath('./src/file1'), 'C:\\base\\src\\file1'); - assert.equal(linkHandler.preprocessPath('src\\file2'), 'C:\\base\\src\\file2'); - assert.equal(linkHandler.preprocessPath('~/src/file3'), 'C:\\Users\\Me\\src\\file3'); - assert.equal(linkHandler.preprocessPath('~\\src\\file4'), 'C:\\Users\\Me\\src\\file4'); - assert.equal(linkHandler.preprocessPath('C:\\absolute\\path\\file5'), 'C:\\absolute\\path\\file5'); - assert.equal(linkHandler.preprocessPath('\\\\?\\C:\\absolute\\path\\extended\\file6'), 'C:\\absolute\\path\\extended\\file6'); - }); - test('Windows - spaces', () => { - const linkHandler: TestTerminalLinkManager = instantiationService.createInstance(TestTerminalLinkManager, new XtermTerminal() as any, { - os: OperatingSystem.Windows, - userHome: 'C:\\Users\\M e' - } as any); - linkHandler.processCwd = 'C:\\base dir'; - - assert.equal(linkHandler.preprocessPath('./src/file1'), 'C:\\base dir\\src\\file1'); - assert.equal(linkHandler.preprocessPath('src\\file2'), 'C:\\base dir\\src\\file2'); - assert.equal(linkHandler.preprocessPath('~/src/file3'), 'C:\\Users\\M e\\src\\file3'); - assert.equal(linkHandler.preprocessPath('~\\src\\file4'), 'C:\\Users\\M e\\src\\file4'); - assert.equal(linkHandler.preprocessPath('C:\\abso lute\\path\\file5'), 'C:\\abso lute\\path\\file5'); - }); - - test('Linux', () => { - const linkHandler: TestTerminalLinkManager = instantiationService.createInstance(TestTerminalLinkManager, new XtermTerminal() as any, { - os: OperatingSystem.Linux, - userHome: '/home/me' - } as any); - linkHandler.processCwd = '/base'; - - assert.equal(linkHandler.preprocessPath('./src/file1'), '/base/src/file1'); - assert.equal(linkHandler.preprocessPath('src/file2'), '/base/src/file2'); - assert.equal(linkHandler.preprocessPath('~/src/file3'), '/home/me/src/file3'); - assert.equal(linkHandler.preprocessPath('/absolute/path/file4'), '/absolute/path/file4'); - }); - - test('No Workspace', () => { - const linkHandler: TestTerminalLinkManager = instantiationService.createInstance(TestTerminalLinkManager, new XtermTerminal() as any, { - os: OperatingSystem.Linux, - userHome: '/home/me' - } as any); - - assert.equal(linkHandler.preprocessPath('./src/file1'), null); - assert.equal(linkHandler.preprocessPath('src/file2'), null); - assert.equal(linkHandler.preprocessPath('~/src/file3'), '/home/me/src/file3'); - assert.equal(linkHandler.preprocessPath('/absolute/path/file4'), '/absolute/path/file4'); - }); - }); - - suite('wrapLinkHandler', () => { - const nullMouseEvent: any = Object.freeze({ preventDefault: () => { } }); - - test('should allow intercepting of links with onBeforeHandleLink', async () => { - const linkHandler: TestTerminalLinkManager = instantiationService.createInstance(TestTerminalLinkManager, new XtermTerminal() as any, { - os: OperatingSystem.Linux, - userHome: '' - } as any); - linkHandler.onBeforeHandleLink(e => { - if (e.link === 'https://www.microsoft.com') { - intercepted = true; - e.resolve(true); - } - e.resolve(false); - }); - const wrappedHandler = linkHandler.wrapLinkHandler(() => defaultHandled = true); - - let defaultHandled = false; - let intercepted = false; - await wrappedHandler(nullMouseEvent, 'https://www.visualstudio.com'); - assert.equal(intercepted, false); - assert.equal(defaultHandled, true); - - defaultHandled = false; - intercepted = false; - await wrappedHandler(nullMouseEvent, 'https://www.microsoft.com'); - assert.equal(intercepted, true); - assert.equal(defaultHandled, false); - }); - }); -}); diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts index fad3e60d818..eed4382af0a 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts @@ -21,7 +21,7 @@ suite('Workbench - TerminalConfigHelper', () => { // const configurationService = new TestConfigurationService(); // configurationService.setUserConfiguration('editor', { fontFamily: 'foo' }); // configurationService.setUserConfiguration('terminal', { integrated: { fontFamily: 'bar' } }); - // const configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!); + // const configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!); // configHelper.panelContainer = fixture; // assert.equal(configHelper.getFont().fontFamily, 'bar', 'terminal.integrated.fontFamily should be selected over editor.fontFamily'); // }); @@ -30,7 +30,8 @@ suite('Workbench - TerminalConfigHelper', () => { const configurationService = new TestConfigurationService(); configurationService.setUserConfiguration('editor', { fontFamily: 'foo' }); configurationService.setUserConfiguration('terminal', { integrated: { fontFamily: null } }); - const configHelper = new TerminalConfigHelper(LinuxDistro.Fedora, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + const configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper.setLinuxDistro(LinuxDistro.Fedora); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontFamily, '\'DejaVu Sans Mono\', monospace', 'Fedora should have its font overridden when terminal.integrated.fontFamily not set'); }); @@ -39,7 +40,8 @@ suite('Workbench - TerminalConfigHelper', () => { const configurationService = new TestConfigurationService(); configurationService.setUserConfiguration('editor', { fontFamily: 'foo' }); configurationService.setUserConfiguration('terminal', { integrated: { fontFamily: null } }); - const configHelper = new TerminalConfigHelper(LinuxDistro.Ubuntu, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + const configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper.setLinuxDistro(LinuxDistro.Ubuntu); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontFamily, '\'Ubuntu Mono\', monospace', 'Ubuntu should have its font overridden when terminal.integrated.fontFamily not set'); }); @@ -48,7 +50,7 @@ suite('Workbench - TerminalConfigHelper', () => { const configurationService = new TestConfigurationService(); configurationService.setUserConfiguration('editor', { fontFamily: 'foo' }); configurationService.setUserConfiguration('terminal', { integrated: { fontFamily: null } }); - const configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + const configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontFamily, 'foo', 'editor.fontFamily should be the fallback when terminal.integrated.fontFamily not set'); }); @@ -66,7 +68,7 @@ suite('Workbench - TerminalConfigHelper', () => { fontSize: 10 } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontSize, 10, 'terminal.integrated.fontSize should be selected over editor.fontSize'); @@ -79,11 +81,12 @@ suite('Workbench - TerminalConfigHelper', () => { fontSize: 0 } }); - configHelper = new TerminalConfigHelper(LinuxDistro.Ubuntu, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper.setLinuxDistro(LinuxDistro.Ubuntu); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontSize, 8, 'The minimum terminal font size (with adjustment) should be used when terminal.integrated.fontSize less than it'); - configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontSize, 6, 'The minimum terminal font size should be used when terminal.integrated.fontSize less than it'); @@ -96,7 +99,7 @@ suite('Workbench - TerminalConfigHelper', () => { fontSize: 1500 } }); - configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontSize, 25, 'The maximum terminal font size should be used when terminal.integrated.fontSize more than it'); @@ -109,11 +112,12 @@ suite('Workbench - TerminalConfigHelper', () => { fontSize: null } }); - configHelper = new TerminalConfigHelper(LinuxDistro.Ubuntu, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper.setLinuxDistro(LinuxDistro.Ubuntu); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontSize, EDITOR_FONT_DEFAULTS.fontSize + 2, 'The default editor font size (with adjustment) should be used when terminal.integrated.fontSize is not set'); - configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontSize, EDITOR_FONT_DEFAULTS.fontSize, 'The default editor font size should be used when terminal.integrated.fontSize is not set'); }); @@ -131,7 +135,7 @@ suite('Workbench - TerminalConfigHelper', () => { lineHeight: 2 } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().lineHeight, 2, 'terminal.integrated.lineHeight should be selected over editor.lineHeight'); @@ -145,7 +149,7 @@ suite('Workbench - TerminalConfigHelper', () => { lineHeight: 0 } }); - configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().lineHeight, 1, 'editor.lineHeight should be 1 when terminal.integrated.lineHeight not set'); }); @@ -158,7 +162,7 @@ suite('Workbench - TerminalConfigHelper', () => { } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.configFontIsMonospace(), true, 'monospace is monospaced'); }); @@ -170,7 +174,7 @@ suite('Workbench - TerminalConfigHelper', () => { fontFamily: 'sans-serif' } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.configFontIsMonospace(), false, 'sans-serif is not monospaced'); }); @@ -182,7 +186,7 @@ suite('Workbench - TerminalConfigHelper', () => { fontFamily: 'serif' } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.configFontIsMonospace(), false, 'serif is not monospaced'); }); @@ -198,7 +202,7 @@ suite('Workbench - TerminalConfigHelper', () => { } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.configFontIsMonospace(), true, 'monospace is monospaced'); }); @@ -214,7 +218,7 @@ suite('Workbench - TerminalConfigHelper', () => { } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.configFontIsMonospace(), false, 'sans-serif is not monospaced'); }); @@ -230,7 +234,7 @@ suite('Workbench - TerminalConfigHelper', () => { } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.configFontIsMonospace(), false, 'serif is not monospaced'); }); diff --git a/src/vs/workbench/contrib/terminal/test/node/terminalEnvironment.test.ts b/src/vs/workbench/contrib/terminal/test/node/terminalEnvironment.test.ts index 78d6cee648e..2ced826aa57 100644 --- a/src/vs/workbench/contrib/terminal/test/node/terminalEnvironment.test.ts +++ b/src/vs/workbench/contrib/terminal/test/node/terminalEnvironment.test.ts @@ -45,16 +45,22 @@ suite('Workbench - TerminalEnvironment', () => { test('auto', () => { assert.equal(shouldSetLangEnvVariable({}, 'auto'), true); assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US' }, 'auto'), true); + assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.utf' }, 'auto'), true); + assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.utf8' }, 'auto'), false); assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.UTF-8' }, 'auto'), false); }); test('off', () => { assert.equal(shouldSetLangEnvVariable({}, 'off'), false); assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US' }, 'off'), false); + assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.utf' }, 'off'), false); + assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.utf8' }, 'off'), false); assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.UTF-8' }, 'off'), false); }); test('on', () => { assert.equal(shouldSetLangEnvVariable({}, 'on'), true); assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US' }, 'on'), true); + assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.utf' }, 'on'), true); + assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.utf8' }, 'on'), true); assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.UTF-8' }, 'on'), true); }); }); diff --git a/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css b/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css index 7d965ad50da..1a41705614c 100644 --- a/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css +++ b/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css @@ -7,26 +7,6 @@ position: relative; } -.monaco-workbench .timeline-view.pane-header .description { - display: block; - font-weight: normal; - margin-left: 10px; - opacity: 0.6; - overflow: hidden; - text-overflow: ellipsis; - text-transform: none; - white-space: nowrap; -} - -.monaco-workbench .timeline-view.pane-header:not(.expanded) .description { - display: none; -} - -.monaco-workbench .timeline-view.pane-header .description span.codicon { - font-size: 9px; - margin-left: 2px; -} - .monaco-workbench .timeline-tree-view .message.timeline-subtle { opacity: 0.5; padding: 10px 22px 0 22px; diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 153a46de95e..8ba8fc64af5 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -219,7 +219,6 @@ export class TimelinePane extends ViewPane { private $container!: HTMLElement; private $message!: HTMLDivElement; - private $titleDescription!: HTMLSpanElement; private $tree!: HTMLDivElement; private tree!: WorkbenchObjectTree; private treeRenderer: TimelineTreeRenderer | undefined; @@ -276,7 +275,7 @@ export class TimelinePane extends ViewPane { this._followActiveEditor = value; this.followActiveEditorContext.set(value); - this.titleDescription = this.titleDescription; + this.updateFilename(this._filename); if (value) { this.onActiveEditorChanged(); @@ -315,7 +314,7 @@ export class TimelinePane extends ViewPane { } this.uri = uri; - this.titleDescription = uri ? basename(uri.fsPath) : ''; + this.updateFilename(uri ? basename(uri.fsPath) : undefined); this.treeRenderer?.setUri(uri); this.loadTimeline(true); } @@ -407,17 +406,13 @@ export class TimelinePane extends ViewPane { } } - private _titleDescription: string | undefined; - get titleDescription(): string | undefined { - return this._titleDescription; - } - - set titleDescription(description: string | undefined) { - this._titleDescription = description; - if (this.followActiveEditor || !description) { - this.$titleDescription.textContent = description ?? ''; + private _filename: string | undefined; + updateFilename(filename: string | undefined) { + this._filename = filename; + if (this.followActiveEditor || !filename) { + this.updateTitleDescription(filename); } else { - this.$titleDescription.textContent = `${description} (pinned)`; + this.updateTitleDescription(`${filename} (pinned)`); } } @@ -781,17 +776,17 @@ export class TimelinePane extends ViewPane { this._isEmpty = !this.hasVisibleItems; if (this.uri === undefined) { - this.titleDescription = undefined; + this.updateFilename(undefined); this.message = localize('timeline.editorCannotProvideTimeline', "The active editor cannot provide timeline information."); } else if (this._isEmpty) { if (this.pendingRequests.size !== 0) { this.setLoadingUriMessage(); } else { - this.titleDescription = basename(this.uri.fsPath); + this.updateFilename(basename(this.uri.fsPath)); this.message = localize('timeline.noTimelineInfo', "No timeline information was provided."); } } else { - this.titleDescription = basename(this.uri.fsPath); + this.updateFilename(basename(this.uri.fsPath)); this.message = undefined; } @@ -849,7 +844,6 @@ export class TimelinePane extends ViewPane { super.renderHeaderTitle(container, this.title); DOM.addClass(container, 'timeline-view'); - this.$titleDescription = DOM.append(container, DOM.$('span.description', undefined, this.titleDescription ?? '')); } protected renderBody(container: HTMLElement): void { @@ -956,7 +950,7 @@ export class TimelinePane extends ViewPane { setLoadingUriMessage() { const file = this.uri && basename(this.uri.fsPath); - this.titleDescription = file ?? ''; + this.updateFilename(file); this.message = file ? localize('timeline.loading', "Loading timeline for {0}...", file) : ''; } diff --git a/src/vs/workbench/contrib/timeline/common/timelineService.ts b/src/vs/workbench/contrib/timeline/common/timelineService.ts index 2d2d5e76d5a..1e6dde4eb70 100644 --- a/src/vs/workbench/contrib/timeline/common/timelineService.ts +++ b/src/vs/workbench/contrib/timeline/common/timelineService.ts @@ -27,7 +27,6 @@ export class TimelineService implements ITimelineService { private readonly _onDidChangeUri = new Emitter(); readonly onDidChangeUri: Event = this._onDidChangeUri.event; - private excludedSources: Set; private readonly hasProviderContext: IContextKey; private readonly providers = new Map(); private readonly providerSubscriptions = new Map(); @@ -39,16 +38,6 @@ export class TimelineService implements ITimelineService { @IContextKeyService protected contextKeyService: IContextKeyService, ) { this.hasProviderContext = TimelineHasProviderContext.bindTo(this.contextKeyService); - - this.excludedSources = new Set(configurationService.getValue('timeline.excludeSources')); - configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('timeline.excludeSources')) { - this.excludedSources = new Set(this.configurationService.getValue('timeline.excludeSources')); - - this.updateHasProviderContext(); - } - }, this); - this.updateHasProviderContext(); // let source = 'fast-source'; @@ -271,12 +260,6 @@ export class TimelineService implements ITimelineService { } private updateHasProviderContext() { - if (this.providers.size === 0) { - this.hasProviderContext.set(false); - return; - } - - const hasProviders = [...this.providers.keys()].some(id => !this.excludedSources.has(id)); - this.hasProviderContext.set(hasProviders); + this.hasProviderContext.set(this.providers.size !== 0); } } diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index 5d590de2e93..8c45b22c10a 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -14,7 +14,6 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IUpdateService, State as UpdateState, StateType, IUpdate } from 'vs/platform/update/common/update'; -import * as semver from 'semver-umd'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -131,7 +130,7 @@ export class ProductContribution implements IWorkbenchContribution { @IHostService hostService: IHostService, @IProductService productService: IProductService ) { - hostService.hadLastFocus().then(hadLastFocus => { + hostService.hadLastFocus().then(async hadLastFocus => { if (!hadLastFocus) { return; } @@ -160,6 +159,7 @@ export class ProductContribution implements IWorkbenchContribution { } // should we show the new license? + const semver = await import('semver-umd'); if (productService.licenseUrl && lastVersion && semver.satisfies(lastVersion, '<1.0.0') && semver.satisfies(productService.version, '>=1.0.0')) { notificationService.info(nls.localize('licenseChanged', "Our license terms have changed, please click [here]({0}) to go through them.", productService.licenseUrl)); } @@ -435,7 +435,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu group: '6_update', command: { id: 'update.downloadNow', - title: nls.localize('download update', "Download Update") + title: nls.localize('download update_1', "Download Update (1)") }, when: CONTEXT_UPDATE_STATE.isEqualTo(StateType.AvailableForDownload) }); @@ -456,7 +456,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu group: '6_update', command: { id: 'update.install', - title: nls.localize('installUpdate...', "Install Update...") + title: nls.localize('installUpdate...', "Install Update... (1)") }, when: CONTEXT_UPDATE_STATE.isEqualTo(StateType.Downloaded) }); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts deleted file mode 100644 index 9b326f949d5..00000000000 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts +++ /dev/null @@ -1,42 +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 { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; -import { Event } from 'vs/base/common/event'; -import { UserDataAutoSyncService as BaseUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger'; -import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; - -export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { - - constructor( - @IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, - @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, - @IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, - @IUserDataSyncService userDataSyncService: IUserDataSyncService, - @IUserDataSyncLogService logService: IUserDataSyncLogService, - @IUserDataSyncAccountService authTokenService: IUserDataSyncAccountService, - @IInstantiationService instantiationService: IInstantiationService, - @IHostService hostService: IHostService, - @ITelemetryService telemetryService: ITelemetryService, - @IUserDataSyncMachinesService userDataSyncMachinesService: IUserDataSyncMachinesService, - @IStorageService storageService: IStorageService, - @IEnvironmentService environmentService: IEnvironmentService, - ) { - super(userDataSyncStoreManagementService, userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, userDataSyncMachinesService, storageService, environmentService); - - this._register(Event.debounce(Event.any( - Event.map(hostService.onDidChangeFocus, () => 'windowFocus'), - instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync, - ), (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, true))); - } - -} diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts index 9a055fc2d7a..8701765c53c 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts @@ -13,6 +13,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { isWeb } from 'vs/base/common/platform'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger'; class UserDataSyncReportIssueContribution extends Disposable implements IWorkbenchContribution { @@ -67,6 +68,7 @@ export class UserDataSyncSettingsMigrationContribution implements IWorkbenchCont const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(UserDataSyncWorkbenchContribution, LifecyclePhase.Ready); workbenchRegistry.registerWorkbenchContribution(UserDataSyncSettingsMigrationContribution, LifecyclePhase.Eventually); +workbenchRegistry.registerWorkbenchContribution(UserDataSyncTrigger, LifecyclePhase.Eventually); if (isWeb) { workbenchRegistry.registerWorkbenchContribution(UserDataSyncReportIssueContribution, LifecyclePhase.Ready); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 049be0714d3..d8625ab699d 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -326,7 +326,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo if (isEqual(this.userDataSyncStoreManagementService.userDataSyncStore?.url, this.userDataSyncStoreManagementService.userDataSyncStore?.insidersUrl)) { this.notificationService.notify({ severity: Severity.Info, - message: localize('switched to insiders', "Settings sync now uses a separate service, more information is available in the [release notes](command:update.showCurrentReleaseNotes)."), + message: localize('switched to insiders', "Settings sync now uses a separate service, more information is available in the [release notes](https://code.visualstudio.com/updates/v1_48#_settings-sync)."), }); } return; @@ -1031,7 +1031,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); } run(accessor: ServicesAccessor): Promise { - return that.userDataAutoSyncService.triggerSync([syncNowCommand.id], false); + return that.userDataAutoSyncService.triggerSync([syncNowCommand.id], false, true); } })); } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts index 36caf6f3db4..9cb6e200cd3 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Emitter } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { SettingsEditor2Input, KeybindingsEditorInput, PreferencesEditorInput } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; @@ -12,24 +12,36 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { IEditorInput } from 'vs/workbench/common/editor'; import { IViewsService } from 'vs/workbench/common/views'; +import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { isWeb } from 'vs/base/common/platform'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; -export class UserDataSyncTrigger extends Disposable { - - private readonly _onDidTriggerSync: Emitter = this._register(new Emitter()); - readonly onDidTriggerSync: Event = this._onDidTriggerSync.event; +export class UserDataSyncTrigger extends Disposable implements IWorkbenchContribution { constructor( @IEditorService editorService: IEditorService, @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, @IViewsService viewsService: IViewsService, + @IUserDataAutoSyncService userDataAutoSyncService: IUserDataAutoSyncService, + @IHostService hostService: IHostService, ) { super(); - this._register( - Event.filter( - Event.any( - Event.map(editorService.onDidActiveEditorChange, () => this.getUserDataEditorInputSource(editorService.activeEditor)), - Event.map(Event.filter(viewsService.onDidChangeViewContainerVisibility, e => e.id === VIEWLET_ID && e.visible), e => e.id) - ), source => source !== undefined)(source => this._onDidTriggerSync.fire(source!))); + const event = Event.filter( + Event.any( + Event.map(editorService.onDidActiveEditorChange, () => this.getUserDataEditorInputSource(editorService.activeEditor)), + Event.map(Event.filter(viewsService.onDidChangeViewContainerVisibility, e => e.id === VIEWLET_ID && e.visible), e => e.id) + ), source => source !== undefined); + if (isWeb) { + this._register(Event.debounce( + Event.any( + Event.map(hostService.onDidChangeFocus, () => 'windowFocus'), + Event.map(event, source => source!), + ), (last, source) => last ? [...last, source] : [source], 1000) + (sources => userDataAutoSyncService.triggerSync(sources, true, false))); + } else { + this._register(event(source => userDataAutoSyncService.triggerSync([source!], true, false))); + } } private getUserDataEditorInputSource(editorInput: IEditorInput | undefined): string | undefined { diff --git a/src/vs/workbench/contrib/views/browser/treeView.ts b/src/vs/workbench/contrib/views/browser/treeView.ts index b13c36fadfb..c620932f2cf 100644 --- a/src/vs/workbench/contrib/views/browser/treeView.ts +++ b/src/vs/workbench/contrib/views/browser/treeView.ts @@ -38,8 +38,9 @@ import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; -import { IHoverService, IHoverOptions } from 'vs/workbench/services/hover/browser/hover'; +import { IHoverService, IHoverOptions, IHoverTarget } from 'vs/workbench/services/hover/browser/hover'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; class Root implements ITreeItem { label = { label: 'root' }; @@ -766,7 +767,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer('explorer.decorations'); templateData.resourceLabel.setResource({ name: label, description, resource: resource ? resource : URI.parse('missing:_icon_resource') }, { fileKind: this.getFileKind(node), - title, + title: undefined, hideIcon: !!iconUrl, fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'], @@ -775,7 +776,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer{ $treeViewId: this.treeViewId, $treeItemHandle: node.handle }; templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false }); @@ -811,10 +812,6 @@ class TreeRenderer extends Disposable implements ITreeRenderer { - await resolvableNode.resolve(); - const tooltip = resolvableNode.tooltip ?? label; + if (node instanceof ResolvableTreeItem) { + await node.resolve(); + } + let tooltip: IMarkdownString | undefined; + if (node.tooltip && !isString(node.tooltip)) { + tooltip = node.tooltip; + } else { + const text = node.tooltip ?? label; + tooltip = text ? new MarkdownString().appendText(text) : undefined; + } if (isHovering && tooltip) { if (!hoverOptions) { - hoverOptions = { text: isString(tooltip) ? { value: tooltip } : tooltip, target: this }; + const target: IHoverTarget = { + targetElements: [this], + dispose: () => { } + }; + hoverOptions = { text: isString(tooltip) ? { value: tooltip } : tooltip, target }; } + (hoverOptions.target).x = e.x; hoverService.showHover(hoverOptions); } this.removeEventListener(DOM.EventType.MOUSE_LEAVE, mouseLeave); @@ -901,7 +911,7 @@ class Aligner extends Disposable { if (this._tree) { const parent: ITreeItem = this._tree.getParentElement(treeItem) || this._tree.getInput(); if (this.hasIcon(parent)) { - return false; + return !!parent.children && parent.children.some(c => c.collapsibleState !== TreeItemCollapsibleState.None && !this.hasIcon(c)); } return !!parent.children && parent.children.every(c => c.collapsibleState === TreeItemCollapsibleState.None || !this.hasIcon(c)); } else { diff --git a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts index 0291f0c605d..b9dcaf3f964 100644 --- a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts @@ -74,15 +74,14 @@ export abstract class BaseWebview extends Disposable { protected get element(): T | undefined { return this._element; } private _focused: boolean | undefined; - protected get focused(): boolean { return !!this._focused; } + public get isFocused(): boolean { return !!this._focused; } private _state: WebviewState.State = new WebviewState.Initializing([]); protected content: WebviewContent; constructor( - // TODO: matb, this should not be protected. The only reason it needs to be is that the base class ends up using it in the call to createElement - protected readonly id: string, + public readonly id: string, options: WebviewOptions, contentOptions: WebviewContentOptions, public readonly extension: WebviewExtensionDescription | undefined, diff --git a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts index ca03dc90b08..7d2046676ff 100644 --- a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts +++ b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts @@ -39,7 +39,7 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv private _findWidgetVisible: IContextKey; public constructor( - private readonly id: string, + public readonly id: string, initialOptions: WebviewOptions, initialContentOptions: WebviewContentOptions, public readonly extension: WebviewExtensionDescription | undefined, @@ -55,6 +55,10 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv this._findWidgetVisible = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE.bindTo(_contextKeyService); } + public get isFocused() { + return !!this._webview.value?.isFocused; + } + private readonly _onDispose = this._register(new Emitter()); public onDispose = this._onDispose.event; diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index 630d5d93422..41e39ecd9e2 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -19,6 +19,11 @@ (function () { 'use strict'; + const isSafari = navigator.vendor && navigator.vendor.indexOf('Apple') > -1 && + navigator.userAgent && + navigator.userAgent.indexOf('CriOS') === -1 && + navigator.userAgent.indexOf('FxiOS') === -1; + /** * Use polling to track focus of main webview and iframes within the webview * @@ -480,7 +485,7 @@ const newFrame = document.createElement('iframe'); newFrame.setAttribute('id', 'pending-frame'); newFrame.setAttribute('frameborder', '0'); - newFrame.setAttribute('sandbox', options.allowScripts ? 'allow-scripts allow-forms allow-same-origin' : 'allow-same-origin'); + newFrame.setAttribute('sandbox', options.allowScripts ? 'allow-scripts allow-forms allow-same-origin allow-pointer-lock' : 'allow-same-origin allow-pointer-lock'); if (host.fakeLoad) { // We should just be able to use srcdoc, but I wasn't // seeing the service worker applying properly. @@ -514,7 +519,7 @@ }, 0); } - if (host.fakeLoad) { + if (host.fakeLoad && !options.allowScripts && isSafari) { // On Safari for iframes with scripts disabled, the `DOMContentLoaded` never seems to be fired. // Use polling instead. const interval = setInterval(() => { @@ -524,7 +529,7 @@ return; } - if (newFrame.contentDocument.readyState === 'complete') { + if (newFrame.contentDocument.readyState !== 'loading') { clearInterval(interval); onFrameLoaded(newFrame.contentDocument); } diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index 969dd6728bc..4df30f8be7b 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -86,6 +86,9 @@ export interface IDataLinkClickEvent { } export interface Webview extends IDisposable { + + readonly id: string; + html: string; contentOptions: WebviewContentOptions; localResourcesRoot: URI[]; @@ -93,6 +96,8 @@ export interface Webview extends IDisposable { initialScrollProgress: number; state: string | undefined; + readonly isFocused: boolean; + readonly onDidFocus: Event; readonly onDidBlur: Event; readonly onDidClickLink: Event; diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditor.ts b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts index 2b4b56fa148..5777d6d5c5b 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts @@ -11,9 +11,9 @@ import { isWeb } from 'vs/base/common/platform'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorDropService } from 'vs/workbench/services/editor/browser/editorDropService'; -import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -21,7 +21,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; -export class WebviewEditor extends BaseEditor { +export class WebviewEditor extends EditorPane { public static readonly ID = 'WebviewEditor'; @@ -107,7 +107,7 @@ export class WebviewEditor extends BaseEditor { super.clearInput(); } - public async setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise { + public async setInput(input: EditorInput, options: EditorOptions, context: IEditorOpenContext, token: CancellationToken): Promise { if (input.matches(this.input)) { return; } @@ -117,7 +117,7 @@ export class WebviewEditor extends BaseEditor { this.webview.release(this); } - await super.setInput(input, options, token); + await super.setInput(input, options, context, token); await input.resolve(); if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 792d98a57bb..993b0dfc59a 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -54,7 +54,7 @@ export class IFrameWebview extends BaseWebview implements Web this._register(this.on(WebviewMessageChannels.loadResource, (entry: any) => { const rawPath = entry.path; const normalizedPath = decodeURIComponent(rawPath); - const uri = URI.parse(normalizedPath.replace(/^\/(\w+)\/(.+)$/, (_, scheme, path) => scheme + ':/' + path)); + const uri = URI.parse(normalizedPath.replace(/^\/([\w\-]+)\/(.+)$/, (_, scheme, path) => scheme + ':/' + path)); this.loadResource(rawPath, uri); })); diff --git a/src/vs/workbench/contrib/webview/browser/webviewIconManager.ts b/src/vs/workbench/contrib/webview/browser/webviewIconManager.ts index 6091fed2b34..e57c8c16cb0 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewIconManager.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewIconManager.ts @@ -54,7 +54,7 @@ export class WebviewIconManager { try { cssRules.push( `.monaco-workbench.vs ${webviewSelector} { content: ""; background-image: ${dom.asCSSUrl(value.light)}; }`, - `.monaco-workbench.vs-dark ${webviewSelector} { content: ""; background-image: ${dom.asCSSUrl(value.dark)}; }` + `.monaco-workbench.vs-dark ${webviewSelector}, .monaco-workbench.hc-black ${webviewSelector} { content: ""; background-image: ${dom.asCSSUrl(value.dark)}; }` ); } catch { // noop diff --git a/src/vs/workbench/contrib/webview/electron-browser/iframeWebviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/iframeWebviewElement.ts index 6e7570d370a..9e776c2122d 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/iframeWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/iframeWebviewElement.ts @@ -52,7 +52,7 @@ export class ElectronIframeWebview extends IFrameWebview { super(id, options, contentOptions, extension, webviewThemeDataProvider, noficationService, tunnelService, fileService, requestService, telemetryService, environmentService, _workbenchEnvironmentService, _remoteAuthorityResolverService, logService); - this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options, Promise.resolve(undefined))); + this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options)); } protected createElement(options: WebviewOptions, contentOptions: WebviewContentOptions) { @@ -122,7 +122,7 @@ export class ElectronIframeWebview extends IFrameWebview { // Workaround this by debouncing the focus and making sure we are not focused on an input // when we try to re-focus. this._focusDelayer.trigger(async () => { - if (!this.focused || !this.element) { + if (!this.isFocused || !this.element) { return; } diff --git a/src/vs/workbench/contrib/webview/electron-browser/resourceLoading.ts b/src/vs/workbench/contrib/webview/electron-browser/resourceLoading.ts index e2d4a407857..4f62d6314fa 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/resourceLoading.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/resourceLoading.ts @@ -58,7 +58,6 @@ export class WebviewResourceRequestManager extends Disposable { private readonly id: string, private readonly extension: WebviewExtensionDescription | undefined, initialContentOptions: WebviewContentOptions, - getWebContentsId: Promise, @ILogService private readonly _logService: ILogService, @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @@ -79,15 +78,13 @@ export class WebviewResourceRequestManager extends Disposable { const remoteAuthority = environmentService.configuration.remoteAuthority; const remoteConnectionData = remoteAuthority ? remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null; - this._ready = getWebContentsId.then(async (webContentsId) => { - this._logService.debug(`WebviewResourceRequestManager(${this.id}): did-start-loading`); - await this._webviewManagerService.registerWebview(this.id, webContentsId, electronService.windowId, { - extensionLocation: this.extension?.location.toJSON(), - localResourceRoots: this._localResourceRoots.map(x => x.toJSON()), - remoteConnectionData: remoteConnectionData, - portMappings: this._portMappings, - }); - + this._logService.debug(`WebviewResourceRequestManager(${this.id}): did-start-loading`); + this._ready = this._webviewManagerService.registerWebview(this.id, electronService.windowId, { + extensionLocation: this.extension?.location.toJSON(), + localResourceRoots: this._localResourceRoots.map(x => x.toJSON()), + remoteConnectionData: remoteConnectionData, + portMappings: this._portMappings, + }).then(() => { this._logService.debug(`WebviewResourceRequestManager(${this.id}): did register`); }); diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 7421ee686ff..0bf81242f3b 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -142,17 +142,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme this._myLogService.debug(`Webview(${this.id}): init`); - const webviewId = new Promise((resolve, reject) => { - const sub = this._register(addDisposableListener(this.element!, 'dom-ready', once(() => { - if (!this.element) { - reject(); - throw new Error('No element'); - } - resolve(this.element.getWebContentsId()); - sub.dispose(); - }))); - }); - this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options, webviewId)); + this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options)); this._register(addDisposableListener(this.element!, 'dom-ready', once(() => { this._register(ElectronWebviewBasedWebview.getWebviewKeyboardHandler(configurationService, mainProcessService).add(this.element!)); @@ -166,7 +156,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme this._myLogService.debug(`Webview(${this.id}): dom-ready`); // Workaround for https://github.com/electron/electron/issues/14474 - if (this.element && (this.focused || document.activeElement === this.element)) { + if (this.element && (this.isFocused || document.activeElement === this.element)) { this.element.blur(); this.element.focus(); } @@ -312,7 +302,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme // Workaround this by debouncing the focus and making sure we are not focused on an input // when we try to re-focus. this._focusDelayer.trigger(async () => { - if (!this.focused || !this.element) { + if (!this.isFocused || !this.element) { return; } diff --git a/extensions/vscode-web-playground/extension.webpack.config.js b/src/vs/workbench/contrib/webviewView/browser/webviewView.contribution.ts similarity index 57% rename from extensions/vscode-web-playground/extension.webpack.config.js rename to src/vs/workbench/contrib/webviewView/browser/webviewView.contribution.ts index 45600607fc5..9dd05114160 100644 --- a/extensions/vscode-web-playground/extension.webpack.config.js +++ b/src/vs/workbench/contrib/webviewView/browser/webviewView.contribution.ts @@ -3,15 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -//@ts-check +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IWebviewViewService, WebviewViewService } from 'vs/workbench/contrib/webviewView/browser/webviewViewService'; -'use strict'; - -const withDefaults = require('../shared.webpack.config'); - -module.exports = withDefaults({ - context: __dirname, - entry: { - extension: './src/extension.ts' - } -}); +registerSingleton(IWebviewViewService, WebviewViewService, true); diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts new file mode 100644 index 00000000000..31ce142a161 --- /dev/null +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts @@ -0,0 +1,175 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Emitter } from 'vs/base/common/event'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import { setImmediate } from 'vs/base/common/platform'; +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 { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IProgressService } from 'vs/platform/progress/common/progress'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { Memento, MementoObject } from 'vs/workbench/common/memento'; +import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IWebviewService, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; +import { IWebviewViewService } from 'vs/workbench/contrib/webviewView/browser/webviewViewService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +declare const ResizeObserver: any; + +const webviewStateKey = 'webviewState'; + +export class WebviewViewPane extends ViewPane { + + private _webview?: WebviewOverlay; + private _activated = false; + + private _container?: HTMLElement; + private _resizeObserver?: any; + + private readonly memento: Memento; + private readonly viewState: MementoObject; + + constructor( + options: IViewletViewOptions, + @IKeybindingService keybindingService: IKeybindingService, + @IContextMenuService contextMenuService: IContextMenuService, + @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IInstantiationService instantiationService: IInstantiationService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, + @ITelemetryService telemetryService: ITelemetryService, + @IStorageService storageService: IStorageService, + @IExtensionService private readonly extensionService: IExtensionService, + @IProgressService private readonly progressService: IProgressService, + @IWebviewService private readonly webviewService: IWebviewService, + @IWebviewViewService private readonly webviewViewService: IWebviewViewService, + ) { + super({ ...options, titleMenuId: MenuId.ViewTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + + this.memento = new Memento(`webviewView.${this.id}`, storageService); + this.viewState = this.memento.getMemento(StorageScope.WORKSPACE); + + this._register(this.onDidChangeBodyVisibility(() => this.updateTreeVisibility())); + this.updateTreeVisibility(); + } + + private readonly _onDidChangeVisibility = this._register(new Emitter()); + readonly onDidChangeVisibility = this._onDidChangeVisibility.event; + + private readonly _onDispose = this._register(new Emitter()); + readonly onDispose = this._onDispose.event; + + dispose() { + this._onDispose.fire(); + + super.dispose(); + } + + focus(): void { + super.focus(); + this._webview?.focus(); + } + + renderBody(container: HTMLElement): void { + super.renderBody(container); + + this._container = container; + + if (!this._resizeObserver) { + this._resizeObserver = new ResizeObserver(() => { + setImmediate(() => { + if (this._container) { + this._webview?.layoutWebviewOverElement(this._container); + } + }); + }); + + this._register(toDisposable(() => { + this._resizeObserver.disconnect(); + })); + this._resizeObserver.observe(container); + } + } + + public saveState() { + if (this._webview) { + this.viewState[webviewStateKey] = this._webview.state; + } + + this.memento.saveMemento(); + super.saveState(); + } + + protected layoutBody(height: number, width: number): void { + super.layoutBody(height, width); + + if (!this._webview) { + return; + } + + if (this._container) { + this._webview.layoutWebviewOverElement(this._container, { width, height }); + } + } + + private updateTreeVisibility() { + if (this.isBodyVisible()) { + this.activate(); + this._webview?.claim(this); + } else { + this._webview?.release(this); + } + } + + private activate() { + if (!this._activated) { + this._activated = true; + + const webviewId = `webviewView-${this.id.replace(/[^a-z0-9]/gi, '-')}`.toLowerCase(); + const webview = this.webviewService.createWebviewOverlay(webviewId, {}, {}, undefined); + webview.state = this.viewState['webviewState']; + this._webview = webview; + + this._register(toDisposable(() => { + this._webview?.release(this); + })); + + this._register(webview.onDidUpdateState(() => { + this.viewState[webviewStateKey] = webview.state; + })); + + const source = this._register(new CancellationTokenSource()); + + this.withProgress(async () => { + await this.extensionService.activateByEvent(`onView:${this.id}`); + + let self = this; + await this.webviewViewService.resolve(this.id, { + webview, + onDidChangeVisibility: this.onDidChangeBodyVisibility, + onDispose: this.onDispose, + get title() { return self.title; }, + set title(value: string) { self.updateTitle(value); } + }, source.token); + }); + } + } + + private async withProgress(task: () => Promise): Promise { + return this.progressService.withProgress({ location: this.id, delay: 500 }, task); + } +} diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts new file mode 100644 index 00000000000..5de370ac42d --- /dev/null +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewService.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 { CancellationToken } from 'vs/base/common/cancellation'; +import { Event } from 'vs/base/common/event'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; + +export const IWebviewViewService = createDecorator('webviewViewService'); + +export interface WebviewView { + title?: string; + + readonly webview: WebviewOverlay; + + readonly onDidChangeVisibility: Event; + readonly onDispose: Event; +} + +export interface IWebviewViewResolver { + resolve(webviewView: WebviewView, cancellation: CancellationToken): Promise; +} + +export interface IWebviewViewService { + + readonly _serviceBrand: undefined; + + register(type: string, resolver: IWebviewViewResolver): IDisposable; + + resolve(viewType: string, webview: WebviewView, cancellation: CancellationToken): Promise; +} + +export class WebviewViewService extends Disposable implements IWebviewViewService { + + readonly _serviceBrand: undefined; + + private readonly _views = new Map(); + + private readonly _awaitingRevival = new Map void }>(); + + constructor() { + super(); + } + + register(viewType: string, resolver: IWebviewViewResolver): IDisposable { + if (this._views.has(viewType)) { + throw new Error(`View resolver already registered for ${viewType}`); + } + + this._views.set(viewType, resolver); + + const pending = this._awaitingRevival.get(viewType); + if (pending) { + resolver.resolve(pending.webview, CancellationToken.None).then(() => { + this._awaitingRevival.delete(viewType); + pending.resolve(); + }); + } + + return toDisposable(() => { + this._views.delete(viewType); + }); + } + + resolve(viewType: string, webview: WebviewView, cancellation: CancellationToken): Promise { + const resolver = this._views.get(viewType); + if (!resolver) { + if (this._awaitingRevival.has(viewType)) { + throw new Error('View already awaiting revival'); + } + + let resolve: () => void; + const p = new Promise(r => resolve = r); + this._awaitingRevival.set(viewType, { webview, resolve: resolve! }); + return p; + } + + return resolver.resolve(webview, cancellation); + } +} + diff --git a/src/vs/workbench/contrib/welcome/overlay/browser/welcomeOverlay.ts b/src/vs/workbench/contrib/welcome/overlay/browser/welcomeOverlay.ts index 33bdbf1ba9d..0c5abc86c18 100644 --- a/src/vs/workbench/contrib/welcome/overlay/browser/welcomeOverlay.ts +++ b/src/vs/workbench/contrib/welcome/overlay/browser/welcomeOverlay.ts @@ -38,31 +38,31 @@ interface Key { const keys: Key[] = [ { id: 'explorer', - arrow: '←', + arrow: '\u2190', // ← label: localize('welcomeOverlay.explorer', "File explorer"), command: 'workbench.view.explorer' }, { id: 'search', - arrow: '←', + arrow: '\u2190', // ← label: localize('welcomeOverlay.search', "Search across files"), command: 'workbench.view.search' }, { id: 'git', - arrow: '←', + arrow: '\u2190', // ← label: localize('welcomeOverlay.git', "Source code management"), command: 'workbench.view.scm' }, { id: 'debug', - arrow: '←', + arrow: '\u2190', // ← label: localize('welcomeOverlay.debug', "Launch and debug"), command: 'workbench.view.debug' }, { id: 'extensions', - arrow: '←', + arrow: '\u2190', // ← label: localize('welcomeOverlay.extensions', "Manage extensions"), command: 'workbench.view.extensions' }, @@ -74,7 +74,7 @@ const keys: Key[] = [ // }, { id: 'problems', - arrow: '⤹', + arrow: '\u2939', // ⤹ label: localize('welcomeOverlay.problems', "View errors and warnings"), command: 'workbench.actions.view.problems' }, @@ -92,13 +92,13 @@ const keys: Key[] = [ // }, { id: 'commandPalette', - arrow: '↖', + arrow: '\u2196', // ↖ label: localize('welcomeOverlay.commandPalette', "Find and run all commands"), command: ShowAllCommandsAction.ID }, { id: 'notifications', - arrow: '⤵', + arrow: '\u2935', // ⤵ arrowLast: true, label: localize('welcomeOverlay.notifications', "Show notifications"), command: 'notifications.showList' @@ -186,7 +186,7 @@ class WelcomeOverlay extends Disposable { .forEach(({ id, arrow, label, command, arrowLast }) => { const div = dom.append(this._overlay, $(`.key.${id}`)); if (arrow && !arrowLast) { - dom.append(div, $('span.arrow')).innerHTML = arrow; + dom.append(div, $('span.arrow', undefined, arrow)); } dom.append(div, $('span.label')).textContent = label; if (command) { @@ -196,7 +196,7 @@ class WelcomeOverlay extends Disposable { } } if (arrow && arrowLast) { - dom.append(div, $('span.arrow')).innerHTML = arrow; + dom.append(div, $('span.arrow', undefined, arrow)); } }); } diff --git a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts index 6378a16dddf..67c86cd6a89 100644 --- a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts +++ b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts @@ -7,7 +7,7 @@ import 'vs/css!./welcomePage'; import 'vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page'; import { URI } from 'vs/base/common/uri'; import * as strings from 'vs/base/common/strings'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import * as arrays from 'vs/base/common/arrays'; import { WalkThroughInput } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -31,7 +31,7 @@ import { splitName } from 'vs/base/common/labels'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { registerColor, focusBorder, textLinkForeground, textLinkActiveForeground, foreground, descriptionForeground, contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils'; -import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtensionsViewPaneContainer, IExtensionsWorkbenchService, VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { IEditorInputFactory, EditorInput } from 'vs/workbench/common/editor'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { TimeoutTimer } from 'vs/base/common/async'; @@ -46,6 +46,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IProductService } from 'vs/platform/product/common/productService'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; const configurationKey = 'workbench.startupEditor'; const oldConfigurationKey = 'workbench.welcome.enabled'; @@ -221,6 +222,16 @@ const extensionPackStrings: Strings = { extensionNotFound: localize('welcomePage.extensionPackNotFound', "Support for {0} with id {1} could not be found."), }; +CommandsRegistry.registerCommand('workbench.extensions.action.showAzureExtensions', accessor => { + const viewletService = accessor.get(IViewletService); + return viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) + .then(viewlet => { + viewlet.search('@sort:installs azure '); + viewlet.focus(); + }); +}); + /* __GDPR__ "installKeymap" : { "${include}": [ diff --git a/src/vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut.ts b/src/vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut.ts index 50398d48086..f21878522bb 100644 --- a/src/vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut.ts +++ b/src/vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut.ts @@ -17,6 +17,8 @@ import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common import { CancellationToken } from 'vs/base/common/cancellation'; import { IProductService } from 'vs/platform/product/common/productService'; import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; export abstract class AbstractTelemetryOptOut implements IWorkbenchContribution { @@ -33,6 +35,8 @@ export abstract class AbstractTelemetryOptOut implements IWorkbenchContribution @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IProductService private readonly productService: IProductService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IJSONEditingService private readonly jsonEditingService: IJSONEditingService ) { } protected async handleTelemetryOptOut(): Promise { @@ -136,10 +140,10 @@ export abstract class AbstractTelemetryOptOut implements IWorkbenchContribution }, { label: noLabel, - run: () => { + run: async () => { logTelemetry(true); this.configurationService.updateValue('telemetry.enableTelemetry', false); - this.configurationService.updateValue('telemetry.enableCrashReporter', false); + await this.jsonEditingService.write(this.environmentService.argvResource, [{ path: ['enable-crash-reporter'], value: false }], true); } } ], @@ -164,9 +168,11 @@ export class BrowserTelemetryOptOut extends AbstractTelemetryOptOut { @IExperimentService experimentService: IExperimentService, @IConfigurationService configurationService: IConfigurationService, @IExtensionGalleryService galleryService: IExtensionGalleryService, - @IProductService productService: IProductService + @IProductService productService: IProductService, + @IEnvironmentService environmentService: IEnvironmentService, + @IJSONEditingService jsonEditingService: IJSONEditingService ) { - super(storageService, openerService, notificationService, hostService, telemetryService, experimentService, configurationService, galleryService, productService); + super(storageService, openerService, notificationService, hostService, telemetryService, experimentService, configurationService, galleryService, productService, environmentService, jsonEditingService); this.handleTelemetryOptOut(); } diff --git a/src/vs/workbench/contrib/welcome/telemetryOptOut/electron-sandbox/telemetryOptOut.ts b/src/vs/workbench/contrib/welcome/telemetryOptOut/electron-sandbox/telemetryOptOut.ts index 0adc911b514..205e0e00d6a 100644 --- a/src/vs/workbench/contrib/welcome/telemetryOptOut/electron-sandbox/telemetryOptOut.ts +++ b/src/vs/workbench/contrib/welcome/telemetryOptOut/electron-sandbox/telemetryOptOut.ts @@ -13,6 +13,8 @@ import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common import { IProductService } from 'vs/platform/product/common/productService'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { AbstractTelemetryOptOut } from 'vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; export class NativeTelemetryOptOut extends AbstractTelemetryOptOut { @@ -27,9 +29,11 @@ export class NativeTelemetryOptOut extends AbstractTelemetryOptOut { @IConfigurationService configurationService: IConfigurationService, @IExtensionGalleryService galleryService: IExtensionGalleryService, @IProductService productService: IProductService, + @IEnvironmentService environmentService: IEnvironmentService, + @IJSONEditingService jsonEditingService: IJSONEditingService, @IElectronService private readonly electronService: IElectronService ) { - super(storageService, openerService, notificationService, hostService, telemetryService, experimentService, configurationService, galleryService, productService); + super(storageService, openerService, notificationService, hostService, telemetryService, experimentService, configurationService, galleryService, productService, environmentService, jsonEditingService); this.handleTelemetryOptOut(); } diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts index d4378989b5f..ac8ba77a324 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts @@ -10,8 +10,8 @@ import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import * as strings from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { EditorOptions, IEditorMemento } from 'vs/workbench/common/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorOptions, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WalkThroughInput } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -55,7 +55,7 @@ interface IWalkThroughEditorViewState { viewState: IViewState; } -export class WalkThroughPart extends BaseEditor { +export class WalkThroughPart extends EditorPane { static readonly ID: string = 'workbench.editor.walkThroughPart'; @@ -262,15 +262,15 @@ export class WalkThroughPart extends BaseEditor { this.scrollbar.setScrollPosition({ scrollTop: scrollPosition.scrollTop + scrollDimensions.height }); } - setInput(input: WalkThroughInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + setInput(input: WalkThroughInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { if (this.input instanceof WalkThroughInput) { this.saveTextEditorViewState(this.input); } this.contentDisposables = dispose(this.contentDisposables); - this.content.innerHTML = ''; + this.content.innerText = ''; - return super.setInput(input, options, token) + return super.setInput(input, options, context, token) .then(() => { return input.resolve(); }) diff --git a/src/vs/workbench/electron-browser/desktop.main.ts b/src/vs/workbench/electron-browser/desktop.main.ts index 06e8e0137c1..20a22c30d0a 100644 --- a/src/vs/workbench/electron-browser/desktop.main.ts +++ b/src/vs/workbench/electron-browser/desktop.main.ts @@ -17,7 +17,6 @@ import { WorkspaceService } from 'vs/workbench/services/configuration/browser/co import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/nativeKeymapService'; import { INativeWindowConfiguration } from 'vs/platform/windows/node/window'; import { ISingleFolderWorkspaceIdentifier, IWorkspaceInitializationPayload, ISingleFolderWorkspaceInitializationPayload, reviveWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ILogService } from 'vs/platform/log/common/log'; @@ -48,7 +47,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import product from 'vs/platform/product/common/product'; import { NativeResourceIdentityService } from 'vs/platform/resource/node/resourceIdentityServiceImpl'; import { IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; -import { DesktopLogService } from 'vs/workbench/services/log/electron-browser/logService'; +import { NativeLogService } from 'vs/workbench/services/log/electron-browser/logService'; import { IElectronService, ElectronService } from 'vs/platform/electron/electron-sandbox/electron'; class DesktopMain extends Disposable { @@ -77,9 +76,6 @@ class DesktopMain extends Disposable { setZoomFactor(zoomLevelToZoomFactor(zoomLevel)); setZoomLevel(zoomLevel, true /* isTrusted */); setFullscreen(!!this.environmentService.configuration.fullscreen); - - // Keyboard support - KeyboardMapperFactory.INSTANCE._onKeyboardLayoutChanged(); } private reviveUris() { @@ -183,7 +179,7 @@ class DesktopMain extends Disposable { serviceCollection.set(IProductService, productService); // Log - const logService = this._register(new DesktopLogService(this.configuration.windowId, mainProcessService, this.environmentService)); + const logService = this._register(new NativeLogService(this.configuration.windowId, mainProcessService, this.environmentService)); serviceCollection.set(ILogService, logService); // Remote diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 793b2394597..4094d919e43 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -6,24 +6,21 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import * as errors from 'vs/base/common/errors'; -import { equals, deepClone } from 'vs/base/common/objects'; +import { equals } from 'vs/base/common/objects'; import * as DOM from 'vs/base/browser/dom'; import { IAction, Separator } from 'vs/base/common/actions'; import { IFileService } from 'vs/platform/files/common/files'; import { toResource, IUntitledTextResourceEditorInput, SideBySideEditor, pathsToEditors } from 'vs/workbench/common/editor'; import { IEditorService, IResourceEditorInputType } from 'vs/workbench/services/editor/common/editorService'; -import { ITelemetryService, crashReporterIdStorageKey } from 'vs/platform/telemetry/common/telemetry'; -import { IWindowSettings, IOpenFileRequest, IWindowsConfiguration, getTitleBarStyle, IAddFoldersRequest } from 'vs/platform/windows/common/windows'; -import { IRunActionInWindowRequest, IRunKeybindingInWindowRequest, INativeOpenFileRequest } from 'vs/platform/windows/node/window'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IOpenFileRequest, IWindowsConfiguration, getTitleBarStyle, IAddFoldersRequest, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, INativeOpenFileRequest } from 'vs/platform/windows/common/windows'; import { ITitleService } from 'vs/workbench/services/title/common/titleService'; -import { IWorkbenchThemeService, VS_HC_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { applyZoom } from 'vs/platform/windows/electron-sandbox/window'; import { setFullscreen, getZoomLevel } from 'vs/base/browser/browser'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; -import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/nativeKeymapService'; -import { CrashReporterStartOptions } from 'vs/base/parts/sandbox/common/electronTypes'; -import { crashReporter, ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { IMenuService, MenuId, IMenu, MenuItemAction, ICommandAction, SubmenuItemAction, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -33,8 +30,8 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { LifecyclePhase, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IWorkspaceFolderCreationData, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { IIntegrityService } from 'vs/workbench/services/integrity/common/integrity'; -import { isWindows, isMacintosh, isLinux } from 'vs/base/common/platform'; -import { IProductService, IAppCenterConfiguration } from 'vs/platform/product/common/productService'; +import { isWindows, isMacintosh } from 'vs/base/common/platform'; +import { IProductService } from 'vs/platform/product/common/productService'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -46,7 +43,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { MenubarControl } from '../browser/parts/titlebar/menubarControl'; import { ILabelService } from 'vs/platform/label/common/label'; import { IUpdateService } from 'vs/platform/update/common/update'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IStorageService } from 'vs/platform/storage/common/storage'; import { IPreferencesService } from '../services/preferences/common/preferences'; import { IMenubarData, IMenubarMenu, IMenubarKeybinding, IMenubarMenuItemSubmenu, IMenubarMenuItemAction, MenubarMenuItem } from 'vs/platform/menubar/common/menubar'; import { IMenubarService } from 'vs/platform/menubar/electron-sandbox/menubar'; @@ -108,9 +105,8 @@ export class NativeWindow extends Disposable { @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, - @IStorageService private readonly storageService: IStorageService, @IProductService private readonly productService: IProductService, - @IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService, + @IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService ) { super(); @@ -131,7 +127,7 @@ export class NativeWindow extends Disposable { }); // Support runAction event - ipcRenderer.on('vscode:runAction', async (event: unknown, request: IRunActionInWindowRequest) => { + ipcRenderer.on('vscode:runAction', async (event: unknown, request: INativeRunActionInWindowRequest) => { const args: unknown[] = request.args || []; // If we run an action from the touchbar, we fill in the currently active resource @@ -162,7 +158,7 @@ export class NativeWindow extends Disposable { }); // Support runKeybinding event - ipcRenderer.on('vscode:runKeybinding', (event: unknown, request: IRunKeybindingInWindowRequest) => { + ipcRenderer.on('vscode:runKeybinding', (event: unknown, request: INativeRunKeybindingInWindowRequest) => { if (document.activeElement) { this.keybindingService.dispatchByUserSettingsLabel(request.userSettingsLabel, document.activeElement); } @@ -204,24 +200,13 @@ export class NativeWindow extends Disposable { // High Contrast Events ipcRenderer.on('vscode:enterHighContrast', async () => { - const windowConfig = this.configurationService.getValue('window'); - if (windowConfig?.autoDetectHighContrast) { - await this.lifecycleService.when(LifecyclePhase.Ready); - this.themeService.setColorTheme(VS_HC_THEME, undefined); - } + await this.lifecycleService.when(LifecyclePhase.Ready); + this.themeService.setOSHighContrast(true); }); ipcRenderer.on('vscode:leaveHighContrast', async () => { - const windowConfig = this.configurationService.getValue('window'); - if (windowConfig?.autoDetectHighContrast) { - await this.lifecycleService.when(LifecyclePhase.Ready); - this.themeService.restoreColorTheme(); - } - }); - - // keyboard layout changed event - ipcRenderer.on('vscode:keyboardLayoutChanged', () => { - KeyboardMapperFactory.INSTANCE._onKeyboardLayoutChanged(); + await this.lifecycleService.when(LifecyclePhase.Ready); + this.themeService.setOSHighContrast(false); }); // accessibility support changed event @@ -416,23 +401,6 @@ export class NativeWindow extends Disposable { // Touchbar menu (if enabled) this.updateTouchbarMenu(); - - // Crash reporter (if enabled) - if (!this.environmentService.disableCrashReporter && this.configurationService.getValue('telemetry.enableCrashReporter')) { - const companyName = this.productService.crashReporter?.companyName || 'Microsoft'; - const productName = this.productService.crashReporter?.productName || this.productService.nameShort; - - // With a provided crash reporter directory, crashes - // will be stored only locally in that folder - if (this.environmentService.crashReporterDirectory) { - this.setupCrashReporter(companyName, productName, undefined, this.environmentService.crashReporterDirectory); - } - - // With appCenter enabled, crashes will be uploaded - else if (this.productService.appCenter) { - this.setupCrashReporter(companyName, productName, this.productService.appCenter, undefined); - } - } } private setupOpenHandlers(): void { @@ -470,10 +438,10 @@ export class NativeWindow extends Disposable { return (await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority)).authority; } } : undefined; - const tunnel = await this.tunnelService.openTunnel(addressProvider, undefined, portMappingRequest.port); + const tunnel = await this.tunnelService.openTunnel(addressProvider, portMappingRequest.address, portMappingRequest.port); if (tunnel) { return { - resolved: uri.with({ authority: `127.0.0.1:${tunnel.tunnelLocalPort}` }), + resolved: uri.with({ authority: tunnel.localAddress }), dispose: () => tunnel.dispose(), }; } @@ -550,42 +518,6 @@ export class NativeWindow extends Disposable { } } - private async setupCrashReporter(companyName: string, productName: string, appCenter: IAppCenterConfiguration, crashesDirectory: undefined): Promise; - private async setupCrashReporter(companyName: string, productName: string, appCenter: undefined, crashesDirectory: string): Promise; - private async setupCrashReporter(companyName: string, productName: string, appCenter: IAppCenterConfiguration | undefined, crashesDirectory: string | undefined): Promise { - let submitURL: string | undefined = undefined; - if (appCenter) { - submitURL = isWindows ? appCenter[process.arch === 'ia32' ? 'win32-ia32' : 'win32-x64'] : isLinux ? appCenter[`linux-x64`] : appCenter.darwin; - } - - const info = await this.telemetryService.getTelemetryInfo(); - const crashReporterId = this.storageService.get(crashReporterIdStorageKey, StorageScope.GLOBAL)!; - - // base options with product info - const options: CrashReporterStartOptions = { - companyName, - productName, - submitURL: (submitURL?.concat('&uid=', crashReporterId, '&iid=', crashReporterId, '&sid=', info.sessionId)) || '', - extra: { - vscode_version: this.productService.version, - vscode_commit: this.productService.commit || '' - }, - - // If `crashesDirectory` is specified, we do not upload - uploadToServer: !crashesDirectory, - }; - - // start crash reporter in the main process first. - // On windows crashpad excepts a name pipe for the client to connect, - // this pipe is created by crash reporter initialization from the main process, - // changing this order of initialization will cause issues. - // For more info: https://chromium.googlesource.com/crashpad/crashpad/+/HEAD/doc/overview_design.md#normal-registration - await this.electronService.startCrashReporter(options); - - // start crash reporter right here - crashReporter.start(deepClone(options)); - } - private onAddFoldersRequest(request: IAddFoldersRequest): void { // Buffer all pending requests @@ -672,7 +604,7 @@ class NativeMenubarControl extends MenubarControl { @IStorageService storageService: IStorageService, @INotificationService notificationService: INotificationService, @IPreferencesService preferencesService: IPreferencesService, - @IWorkbenchEnvironmentService protected readonly environmentService: INativeWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService, @IAccessibilityService accessibilityService: IAccessibilityService, @IMenubarService private readonly menubarService: IMenubarService, @IHostService hostService: IHostService, @@ -709,13 +641,13 @@ class NativeMenubarControl extends MenubarControl { (async () => { this.recentlyOpened = await this.workspacesService.getRecentlyOpened(); - this.doUpdateMenubar(true); + this.doUpdateMenubar(); })(); this.registerListeners(); } - protected doUpdateMenubar(firstTime: boolean): void { + protected doUpdateMenubar(): void { // Since the native menubar is shared between windows (main process) // only allow the focused window to update the menubar if (!this.hostService.hasFocus) { diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index 0296383f560..07fe262c796 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -258,9 +258,9 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; 'window.autoDetectHighContrast': { 'type': 'boolean', 'default': true, - 'description': nls.localize('autoDetectHighContrast', "If enabled, will automatically change to high contrast theme if Windows is using a high contrast theme, and to dark theme when switching away from a Windows high contrast theme."), + 'description': nls.localize('autoDetectHighContrast', "If enabled, will automatically change to high contrast theme if the OS is using a high contrast theme, and to dark theme when switching away from a high contrast theme."), 'scope': ConfigurationScope.APPLICATION, - 'included': isWindows + 'included': isWindows || isMacintosh }, 'window.doubleClickIconToClose': { 'type': 'boolean', @@ -314,6 +314,31 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; } } }); + + // Keybinding + registry.registerConfiguration({ + 'id': 'keyboard', + 'order': 15, + 'type': 'object', + 'title': nls.localize('keyboardConfigurationTitle', "Keyboard"), + 'properties': { + 'keyboard.touchbar.enabled': { + 'type': 'boolean', + 'default': true, + 'description': nls.localize('touchbar.enabled', "Enables the macOS touchbar buttons on the keyboard if available."), + 'included': isMacintosh + }, + 'keyboard.touchbar.ignored': { + 'type': 'array', + 'items': { + 'type': 'string' + }, + 'default': [], + 'markdownDescription': nls.localize('touchbar.ignored', 'A set of identifiers for entries in the touchbar that should not show up (for example `workbench.action.navigateBack`.'), + 'included': isMacintosh + } + } + }); })(); // JSON Schemas @@ -344,6 +369,14 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; type: 'string', markdownDescription: nls.localize('argv.forceColorProfile', 'Allows to override the color profile to use. If you experience colors appear badly, try to set this to `srgb` and restart.') }, + 'enable-crash-reporter': { + type: 'boolean', + markdownDescription: nls.localize('argv.enableCrashReporter', 'Allows to disable crash reporting, should restart the app if the value is changed.') + }, + 'crash-reporter-id': { + type: 'string', + markdownDescription: nls.localize('argv.crashReporterId', 'Unique id used for correlating crash reports sent from this app instance.') + }, 'enable-proposed-api': { type: 'array', description: nls.localize('argv.enebleProposedApi', "Enable proposed APIs for a list of extension ids (such as \`vscode.git\`). Proposed APIs are unstable and subject to breaking without warning at any time. This should only be set for extension development and testing purposes."), diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts new file mode 100644 index 00000000000..9b4c13f9441 --- /dev/null +++ b/src/vs/workbench/electron-sandbox/desktop.main.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 { zoomLevelToZoomFactor } from 'vs/platform/windows/common/windows'; +import { importEntries, mark } from 'vs/base/common/performance'; +import { Workbench } from 'vs/workbench/browser/workbench'; +import { setZoomLevel, setZoomFactor, setFullscreen } from 'vs/base/browser/browser'; +import { domContentLoaded, addDisposableListener, EventType, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; +import { URI } from 'vs/base/common/uri'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Schemas } from 'vs/base/common/network'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IMainProcessService, MainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { RemoteFileSystemProvider } from 'vs/workbench/services/remote/common/remoteAgentFileSystemChannel'; +import { ISignService } from 'vs/platform/sign/common/sign'; +import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider'; +import { IProductService } from 'vs/platform/product/common/productService'; +import product from 'vs/platform/product/common/product'; +import { IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; +import { IElectronService, ElectronService } from 'vs/platform/electron/electron-sandbox/electron'; +import { SimpleConfigurationService, simpleFileSystemProvider, SimpleLogService, SimpleRemoteAgentService, SimpleRemoteAuthorityResolverService, SimpleResourceIdentityService, SimpleSignService, SimpleStorageService, SimpleWorkspaceService } from 'vs/workbench/electron-sandbox/sandbox.simpleservices'; +import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; + +class DesktopMain extends Disposable { + + private readonly environmentService = new BrowserWorkbenchEnvironmentService({ + logsPath: URI.file('logs-path'), + workspaceId: '' + }); + + constructor(private configuration: any /*INativeWindowConfiguration*/) { + super(); + + this.init(); + } + + private init(): void { + + // Setup perf + importEntries(this.configuration.perfEntries); + + // Browser config + const zoomLevel = this.configuration.zoomLevel || 0; + setZoomFactor(zoomLevelToZoomFactor(zoomLevel)); + setZoomLevel(zoomLevel, true /* isTrusted */); + setFullscreen(!!this.configuration.fullscreen); + } + + async open(): Promise { + const services = await this.initServices(); + + await domContentLoaded(); + mark('willStartWorkbench'); + + // Create Workbench + const workbench = new Workbench(document.body, services.serviceCollection, services.logService); + + // Listeners + this.registerListeners(workbench, services.storageService); + + // Startup + workbench.startup(); + + // Logging + services.logService.trace('workbench configuration', JSON.stringify(this.environmentService.configuration)); + } + + private registerListeners(workbench: Workbench, storageService: SimpleStorageService): void { + + // Layout + this._register(addDisposableListener(window, EventType.RESIZE, e => this.onWindowResize(e, true, workbench))); + + // Workbench Lifecycle + this._register(workbench.onShutdown(() => this.dispose())); + this._register(workbench.onWillShutdown(event => event.join(storageService.close()))); + } + + private onWindowResize(e: Event, retry: boolean, workbench: Workbench): void { + if (e.target === window) { + if (window.document && window.document.body && window.document.body.clientWidth === 0) { + // TODO@Ben this is an electron issue on macOS when simple fullscreen is enabled + // where for some reason the window clientWidth is reported as 0 when switching + // between simple fullscreen and normal screen. In that case we schedule the layout + // call at the next animation frame once, in the hope that the dimensions are + // proper then. + if (retry) { + scheduleAtNextAnimationFrame(() => this.onWindowResize(e, false, workbench)); + } + return; + } + + workbench.layout(); + } + } + + private async initServices(): Promise<{ serviceCollection: ServiceCollection, logService: ILogService, storageService: SimpleStorageService }> { + const serviceCollection = new ServiceCollection(); + + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // NOTE: DO NOT ADD ANY OTHER SERVICE INTO THE COLLECTION HERE. + // CONTRIBUTE IT VIA WORKBENCH.DESKTOP.MAIN.TS AND registerSingleton(). + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + // Main Process + const mainProcessService = this._register(new MainProcessService(this.configuration.windowId)); + serviceCollection.set(IMainProcessService, mainProcessService); + + // Environment + serviceCollection.set(IWorkbenchEnvironmentService, this.environmentService); + + // Product + const productService: IProductService = { _serviceBrand: undefined, ...product }; + serviceCollection.set(IProductService, productService); + + // Log + const logService = new SimpleLogService(); + serviceCollection.set(ILogService, logService); + + // Remote + const remoteAuthorityResolverService = new SimpleRemoteAuthorityResolverService(); + serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService); + + // Sign + const signService = new SimpleSignService(); + serviceCollection.set(ISignService, signService); + + // Remote Agent + const remoteAgentService = new SimpleRemoteAgentService(); + serviceCollection.set(IRemoteAgentService, remoteAgentService); + + // Electron + const electronService = new ElectronService(this.configuration.windowId, mainProcessService) as IElectronService; + serviceCollection.set(IElectronService, electronService); + + // Files + const fileService = this._register(new FileService(logService)); + serviceCollection.set(IFileService, fileService); + + fileService.registerProvider(Schemas.file, simpleFileSystemProvider); + + // User Data Provider + fileService.registerProvider(Schemas.userData, new FileUserDataProvider(URI.file('user-home'), this.environmentService.backupHome, simpleFileSystemProvider, this.environmentService, logService)); + + const connection = remoteAgentService.getConnection(); + if (connection) { + const remoteFileSystemProvider = this._register(new RemoteFileSystemProvider(remoteAgentService)); + fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider); + } + + const resourceIdentityService = new SimpleResourceIdentityService(); + serviceCollection.set(IResourceIdentityService, resourceIdentityService); + + const services = await Promise.all([ + this.createWorkspaceService().then(service => { + + // Workspace + serviceCollection.set(IWorkspaceContextService, service); + + // Configuration + serviceCollection.set(IConfigurationService, new SimpleConfigurationService()); + + return service; + }), + + this.createStorageService().then(service => { + + // Storage + serviceCollection.set(IStorageService, service); + + return service; + }) + ]); + + return { serviceCollection, logService, storageService: services[1] }; + } + + private async createWorkspaceService(): Promise { + return new SimpleWorkspaceService(); + } + + private async createStorageService(): Promise { + return new SimpleStorageService(); + } +} + +export function main(configuration: any /*INativeWindowConfiguration*/): Promise { + const workbench = new DesktopMain(configuration); + + return workbench.open(); +} diff --git a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts new file mode 100644 index 00000000000..596d7b2a8a9 --- /dev/null +++ b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts @@ -0,0 +1,907 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* eslint-disable code-no-standalone-editor */ +/* eslint-disable code-import-patterns */ + +import { ConsoleLogService } from 'vs/platform/log/common/log'; +import { IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; +import { ISignService } from 'vs/platform/sign/common/sign'; +import { hash } from 'vs/base/common/hash'; +import { URI } from 'vs/base/common/uri'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { IRemoteAuthorityResolverService, IRemoteConnectionData, ResolvedAuthority, ResolvedOptions, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { Event } from 'vs/base/common/event'; +import { IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; +import { IAddressProvider, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; +import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { ITelemetryData, ITelemetryInfo, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { BrowserSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory'; +import { ExtensionIdentifier, ExtensionType, IExtension, IExtensionDescription, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { SimpleConfigurationService as BaseSimpleConfigurationService } from 'vs/editor/standalone/browser/simpleServices'; +import { InMemoryStorageService } from 'vs/platform/storage/common/storage'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup'; +import { ITextSnapshot } from 'vs/editor/common/model'; +import { IExtensionService, NullExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ClassifiedEvent, GDPRClassification, StrictPropertyChecker } from 'vs/platform/telemetry/common/gdprTypings'; +import { IKeyboardLayoutInfo, IKeymapService, ILinuxKeyboardLayoutInfo, ILinuxKeyboardMapping, IMacKeyboardLayoutInfo, IMacKeyboardMapping, IWindowsKeyboardLayoutInfo, IWindowsKeyboardMapping } from 'vs/workbench/services/keybinding/common/keymapInfo'; +import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; +import { DispatchConfig } from 'vs/workbench/services/keybinding/common/dispatchConfig'; +import { IKeyboardMapper } from 'vs/workbench/services/keybinding/common/keyboardMapper'; +import { ChordKeybinding, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common/keyCodes'; +import { ScanCodeBinding } from 'vs/base/common/scanCode'; +import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; +import { isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { posix, win32 } from 'vs/base/common/path'; +import { IConfirmation, IConfirmationResult, IDialogOptions, IDialogService, IShowResult } from 'vs/platform/dialogs/common/dialogs'; +import Severity from 'vs/base/common/severity'; +import { IWebviewService, WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewIcons, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { AbstractTextFileService } from 'vs/workbench/services/textfile/browser/textFileService'; +import { EnablementState, ExtensionRecommendationReason, IExtensionManagementServer, IExtensionManagementServerService, IExtensionRecommendation } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { LanguageId, TokenizationRegistry } from 'vs/editor/common/modes'; +import { IGrammar, ITextMateService } from 'vs/workbench/services/textMate/common/textMateService'; +import { AccessibilitySupport, IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { ITunnelProvider, ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { IManualSyncTask, IResourcePreview, ISyncResourceHandle, ISyncTask, IUserDataAutoSyncService, IUserDataSyncService, IUserDataSyncStore, IUserDataSyncStoreManagementService, SyncResource, SyncStatus, UserDataSyncStoreType } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncAccount, IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; +import { AbstractTimerService, IStartupMetrics, ITimerService, Writeable } from 'vs/workbench/services/timer/browser/timerService'; +import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; +import { ISingleFolderWorkspaceIdentifier, IWorkspaceFolderCreationData, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { ITaskProvider, ITaskService, ITaskSummary, ProblemMatcherRunOptions, Task, TaskFilter, TaskTerminateResponse, WorkspaceFolderTaskResult } from 'vs/workbench/contrib/tasks/common/taskService'; +import { Action } from 'vs/base/common/actions'; +import { LinkedMap } from 'vs/base/common/map'; +import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder, WorkbenchState, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { CustomTask, ContributedTask, InMemoryTask, TaskRunSource, ConfiguringTask, TaskIdentifier, TaskSorter } from 'vs/workbench/contrib/tasks/common/tasks'; +import { TaskSystemInfo } from 'vs/workbench/contrib/tasks/common/taskSystem'; +import { IExtensionManagementService, ILocalExtension, IGalleryExtension, IReportedExtension, IGalleryMetadata, IExtensionIdentifier, IExtensionTipsService, IConfigBasedExtensionTip, IExecutableBasedExtensionTip, IWorkspaceTips } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IWorkspaceTagsService, Tags } from 'vs/workbench/contrib/tags/common/workspaceTags'; +import { AsbtractOutputChannelModelService, IOutputChannelModelService } from 'vs/workbench/services/output/common/outputChannelModel'; +import { Color, RGBA } from 'vs/base/common/color'; +import { joinPath } from 'vs/base/common/resources'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; + +//#region Workspace + +export const workspaceResource = URI.file(isWindows ? '\\simpleWorkspace' : '/simpleWorkspace'); + +export class SimpleWorkspaceService implements IWorkspaceContextService { + + declare readonly _serviceBrand: undefined; + + readonly onDidChangeWorkspaceName = Event.None; + readonly onDidChangeWorkspaceFolders = Event.None; + readonly onDidChangeWorkbenchState = Event.None; + + private readonly workspace: IWorkspace; + + constructor() { + this.workspace = { id: '4064f6ec-cb38-4ad0-af64-ee6467e63c82', folders: [new WorkspaceFolder({ uri: workspaceResource, name: '', index: 0 })] }; + } + + async getCompleteWorkspace(): Promise { return this.getWorkspace(); } + + getWorkspace(): IWorkspace { return this.workspace; } + + getWorkbenchState(): WorkbenchState { + if (this.workspace) { + if (this.workspace.configuration) { + return WorkbenchState.WORKSPACE; + } + return WorkbenchState.FOLDER; + } + return WorkbenchState.EMPTY; + } + + getWorkspaceFolder(resource: URI): IWorkspaceFolder | null { return resource && resource.scheme === workspaceResource.scheme ? this.workspace.folders[0] : null; } + isInsideWorkspace(resource: URI): boolean { return resource && resource.scheme === workspaceResource.scheme; } + isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean { return true; } +} + +//#endregion + + +//#region Configuration + +export class SimpleStorageService extends InMemoryStorageService { } + +//#endregion + + +//#region Configuration + +export class SimpleConfigurationService extends BaseSimpleConfigurationService { } + +//#endregion + + +//#region Logger + +export class SimpleLogService extends ConsoleLogService { } + +export class SimpleSignService implements ISignService { + + declare readonly _serviceBrand: undefined; + + async sign(value: string): Promise { return value; } +} + +//#endregion + + +//#region Files + +class SimpleFileSystemProvider extends InMemoryFileSystemProvider { } + +export const simpleFileSystemProvider = new SimpleFileSystemProvider(); + +function createFile(parent: string, name: string, content: string = ''): void { + simpleFileSystemProvider.writeFile(joinPath(workspaceResource, parent, name), VSBuffer.fromString(content).buffer, { create: true, overwrite: true }); +} + +function createFolder(name: string): void { + simpleFileSystemProvider.mkdir(joinPath(workspaceResource, name)); +} + +createFolder(''); +createFolder('src'); +createFolder('test'); + +createFile('', '.gitignore', `out +node_modules +.vscode-test/ +*.vsix +`); + +createFile('', '.vscodeignore', `.vscode/** +.vscode-test/** +out/test/** +src/** +.gitignore +vsc-extension-quickstart.md +**/tsconfig.json +**/tslint.json +**/*.map +**/*.ts`); + +createFile('', 'CHANGELOG.md', `# Change Log +All notable changes to the "test-ts" extension will be documented in this file. + +Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. + +## [Unreleased] +- Initial release`); +createFile('', 'package.json', `{ + "name": "test-ts", + "displayName": "test-ts", + "description": "", + "version": "0.0.1", + "engines": { + "vscode": "^1.31.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "onCommand:extension.helloWorld" + ], + "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "extension.helloWorld", + "title": "Hello World" + } + ] + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "postinstall": "node ./node_modules/vscode/bin/install", + "test": "npm run compile && node ./node_modules/vscode/bin/test" + }, + "devDependencies": { + "typescript": "^3.3.1", + "vscode": "^1.1.28", + "tslint": "^5.12.1", + "@types/node": "^8.10.25", + "@types/mocha": "^2.2.42" + } +} +`); + +createFile('', 'tsconfig.json', `{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "out", + "lib": [ + "es6" + ], + "sourceMap": true, + "rootDir": "src", + "strict": true /* enable all strict type-checking options */ + /* Additional Checks */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + }, + "exclude": [ + "node_modules", + ".vscode-test" + ] +} +`); + +createFile('', 'tslint.json', `{ + "rules": { + "no-string-throw": true, + "no-unused-expression": true, + "no-duplicate-variable": true, + "curly": true, + "class-name": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": true + }, + "defaultSeverity": "warning" +} +`); + +createFile('src', 'extension.ts', `// The module 'vscode' contains the VS Code extensibility API +// Import the module and reference it with the alias vscode in your code below +import * as vscode from 'vscode'; + +// this method is called when your extension is activated +// your extension is activated the very first time the command is executed +export function activate(context: vscode.ExtensionContext) { + + // Use the console to output diagnostic information (console.log) and errors (console.error) + // This line of code will only be executed once when your extension is activated + console.log('Congratulations, your extension "test-ts" is now active!'); + + // The command has been defined in the package.json file + // Now provide the implementation of the command with registerCommand + // The commandId parameter must match the command field in package.json + let disposable = vscode.commands.registerCommand('extension.helloWorld', () => { + // The code you place here will be executed every time your command is executed + + // Display a message box to the user + vscode.window.showInformationMessage('Hello World!'); + }); + + context.subscriptions.push(disposable); +} + +// this method is called when your extension is deactivated +export function deactivate() {} +`); + +createFile('test', 'extension.test.ts', `// +// Note: This example test is leveraging the Mocha test framework. +// Please refer to their documentation on https://mochajs.org/ for help. +// + +// The module 'assert' provides assertion methods from node +import * as assert from 'assert'; + +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +// import * as vscode from 'vscode'; +// import * as myExtension from '../extension'; + +// Defines a Mocha test suite to group tests of similar kind together +suite("Extension Tests", function () { + + // Defines a Mocha unit test + test("Something 1", function() { + assert.equal(-1, [1, 2, 3].indexOf(5)); + assert.equal(-1, [1, 2, 3].indexOf(0)); + }); +});`); + +createFile('test', 'index.ts', `// +// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING +// +// This file is providing the test runner to use when running extension tests. +// By default the test runner in use is Mocha based. +// +// You can provide your own test runner if you want to override it by exporting +// a function run(testRoot: string, clb: (error:Error) => void) that the extension +// host can call to run the tests. The test runner is expected to use console.log +// to report the results back to the caller. When the tests are finished, return +// a possible error to the callback or null if none. + +import * as testRunner from 'vscode/lib/testrunner'; + +// You can directly control Mocha options by configuring the test runner below +// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options +// for more info +testRunner.configure({ + ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) + useColors: true // colored output from test results +}); + +module.exports = testRunner;`); + +//#endregion + + +//#region Resource Identity + +export class SimpleResourceIdentityService implements IResourceIdentityService { + + declare readonly _serviceBrand: undefined; + + async resolveResourceIdentity(resource: URI): Promise { return hash(resource.toString()).toString(16); } +} + +//#endregion + + +//#region Remote + +export class SimpleRemoteAuthorityResolverService implements IRemoteAuthorityResolverService { + + declare readonly _serviceBrand: undefined; + + onDidChangeConnectionData: Event = Event.None; + resolveAuthority(authority: string): Promise { throw new Error('Method not implemented.'); } + getConnectionData(authority: string): IRemoteConnectionData | null { return null; } + _clearResolvedAuthority(authority: string): void { } + _setResolvedAuthority(resolvedAuthority: ResolvedAuthority, resolvedOptions?: ResolvedOptions): void { } + _setResolvedAuthorityError(authority: string, err: any): void { } + _setAuthorityConnectionToken(authority: string, connectionToken: string): void { } +} + +export class SimpleRemoteAgentService implements IRemoteAgentService { + + declare readonly _serviceBrand: undefined; + + socketFactory: ISocketFactory = new BrowserSocketFactory(null); + + getConnection(): IRemoteAgentConnection | null { return null; } + async getEnvironment(bail?: boolean): Promise { return null; } + async getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise { return undefined; } + async disableTelemetry(): Promise { } + async logTelemetry(eventName: string, data?: ITelemetryData): Promise { } + async flushTelemetry(): Promise { } + async getRawEnvironment(): Promise { return null; } + async scanExtensions(skipExtensions?: ExtensionIdentifier[]): Promise { return []; } +} + +//#endregion + + +//#region Backup File + +class SimpleBackupFileService implements IBackupFileService { + + declare readonly _serviceBrand: undefined; + + async hasBackups(): Promise { return false; } + async discardResourceBackup(resource: URI): Promise { } + async discardAllWorkspaceBackups(): Promise { } + toBackupResource(resource: URI): URI { return resource; } + hasBackupSync(resource: URI, versionId?: number): boolean { return false; } + async getBackups(): Promise { return []; } + async resolve(resource: URI): Promise | undefined> { return undefined; } + async backup(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: T): Promise { } + async discardBackup(resource: URI): Promise { } + async discardBackups(): Promise { } +} + +registerSingleton(IBackupFileService, SimpleBackupFileService); + +//#endregion + + +//#region Extensions + +class SimpleExtensionService extends NullExtensionService { } + +registerSingleton(IExtensionService, SimpleExtensionService); + +//#endregion + + +//#region Extensions Workbench (TODO@sandbox TODO@ben remove when 'semver-umd' can be loaded) + +class SimpleExtensionsWorkbenchService implements IExtensionsWorkbenchService { + + declare readonly _serviceBrand: undefined; + + onChange = Event.None; + + local = []; + installed = []; + outdated = []; + + queryGallery(...args: any[]): any { throw new Error('Method not implemented.'); } + install(...args: any[]): any { throw new Error('Method not implemented.'); } + queryLocal(server?: IExtensionManagementServer): Promise { throw new Error('Method not implemented.'); } + canInstall(extension: any): boolean { throw new Error('Method not implemented.'); } + uninstall(extension: any): Promise { throw new Error('Method not implemented.'); } + installVersion(extension: any, version: string): Promise { throw new Error('Method not implemented.'); } + reinstall(extension: any): Promise { throw new Error('Method not implemented.'); } + setEnablement(extensions: any | any[], enablementState: EnablementState): Promise { throw new Error('Method not implemented.'); } + open(extension: any, options?: { sideByside?: boolean | undefined; preserveFocus?: boolean | undefined; pinned?: boolean | undefined; }): Promise { throw new Error('Method not implemented.'); } + checkForUpdates(): Promise { throw new Error('Method not implemented.'); } + isExtensionIgnoredToSync(extension: any): boolean { throw new Error('Method not implemented.'); } + toggleExtensionIgnoredToSync(extension: any): Promise { throw new Error('Method not implemented.'); } +} + +registerSingleton(IExtensionsWorkbenchService, SimpleExtensionsWorkbenchService); + +//#endregion + +//#region Telemetry + +class SimpleTelemetryService implements ITelemetryService { + + declare readonly _serviceBrand: undefined; + + readonly sendErrorTelemetry = false; + readonly isOptedIn = false; + + async publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { } + async publicLog2 = never, T extends GDPRClassification = never>(eventName: string, data?: StrictPropertyChecker, 'Type of classified event does not match event properties'>, anonymizeFilePaths?: boolean): Promise { } + async publicLogError(errorEventName: string, data?: ITelemetryData): Promise { } + async publicLogError2 = never, T extends GDPRClassification = never>(eventName: string, data?: StrictPropertyChecker, 'Type of classified event does not match event properties'>): Promise { } + setEnabled(value: boolean): void { } + setExperimentProperty(name: string, value: string): void { } + async getTelemetryInfo(): Promise { + return { + instanceId: 'someValue.instanceId', + sessionId: 'someValue.sessionId', + machineId: 'someValue.machineId' + }; + } +} + +registerSingleton(ITelemetryService, SimpleTelemetryService); + +//#endregion + + +//#region Keymap Service + +class SimpleKeyboardMapper implements IKeyboardMapper { + dumpDebugInfo(): string { return ''; } + resolveKeybinding(keybinding: ChordKeybinding): ResolvedKeybinding[] { return []; } + resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding { + let keybinding = new SimpleKeybinding( + keyboardEvent.ctrlKey, + keyboardEvent.shiftKey, + keyboardEvent.altKey, + keyboardEvent.metaKey, + keyboardEvent.keyCode + ).toChord(); + return new USLayoutResolvedKeybinding(keybinding, OS); + } + resolveUserBinding(firstPart: (SimpleKeybinding | ScanCodeBinding)[]): ResolvedKeybinding[] { return []; } +} + +class SimpleKeymapService implements IKeymapService { + + declare readonly _serviceBrand: undefined; + + onDidChangeKeyboardMapper = Event.None; + getKeyboardMapper(dispatchConfig: DispatchConfig): IKeyboardMapper { return new SimpleKeyboardMapper(); } + getCurrentKeyboardLayout(): (IWindowsKeyboardLayoutInfo & { isUserKeyboardLayout?: boolean | undefined; isUSStandard?: true | undefined; }) | (ILinuxKeyboardLayoutInfo & { isUserKeyboardLayout?: boolean | undefined; isUSStandard?: true | undefined; }) | (IMacKeyboardLayoutInfo & { isUserKeyboardLayout?: boolean | undefined; isUSStandard?: true | undefined; }) | null { return null; } + getAllKeyboardLayouts(): IKeyboardLayoutInfo[] { return []; } + getRawKeyboardMapping(): IWindowsKeyboardMapping | ILinuxKeyboardMapping | IMacKeyboardMapping | null { return null; } + validateCurrentKeyboardMapping(keyboardEvent: IKeyboardEvent): void { } +} + +registerSingleton(IKeymapService, SimpleKeymapService); + +//#endregion + + +//#region Path + +class SimplePathService implements IPathService { + + declare readonly _serviceBrand: undefined; + + readonly resolvedUserHome = URI.file('user-home'); + readonly path = Promise.resolve(OS === OperatingSystem.Windows ? win32 : posix); + + async fileURI(path: string): Promise { return URI.file(path); } + async userHome(options?: { preferLocal: boolean; }): Promise { return this.resolvedUserHome; } +} + +registerSingleton(IPathService, SimplePathService); + +//#endregion + + +//#region Dialog + +class SimpleDialogService implements IDialogService { + + declare readonly _serviceBrand: undefined; + + async confirm(confirmation: IConfirmation): Promise { return { confirmed: false }; } + async show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise { return { choice: 1 }; } + async about(): Promise { } +} + +registerSingleton(IDialogService, SimpleDialogService); + +//#endregion + + +//#region Webview + +class SimpleWebviewService implements IWebviewService { + declare readonly _serviceBrand: undefined; + + createWebviewElement(id: string, options: WebviewOptions, contentOptions: WebviewContentOptions, extension: WebviewExtensionDescription | undefined): WebviewElement { throw new Error('Method not implemented.'); } + createWebviewOverlay(id: string, options: WebviewOptions, contentOptions: WebviewContentOptions, extension: WebviewExtensionDescription | undefined): WebviewOverlay { throw new Error('Method not implemented.'); } + setIcons(id: string, value: WebviewIcons | undefined): void { } +} + +registerSingleton(IWebviewService, SimpleWebviewService); + +//#endregion + + +//#region Textfiles + +class SimpleTextFileService extends AbstractTextFileService { + declare readonly _serviceBrand: undefined; +} + +registerSingleton(ITextFileService, SimpleTextFileService); + +//#endregion + + +//#region extensions management + +class SimpleExtensionManagementServerService implements IExtensionManagementServerService { + + declare readonly _serviceBrand: undefined; + + readonly localExtensionManagementServer = null; + readonly remoteExtensionManagementServer = null; + readonly webExtensionManagementServer = null; + + getExtensionManagementServer(extension: IExtension): IExtensionManagementServer | null { return null; } +} + +registerSingleton(IExtensionManagementServerService, SimpleExtensionManagementServerService); + +//#endregion + + +//#region Textmate + +TokenizationRegistry.setColorMap([null!, new Color(new RGBA(212, 212, 212, 1)), new Color(new RGBA(30, 30, 30, 1))]); + +class SimpleTextMateService implements ITextMateService { + + declare readonly _serviceBrand: undefined; + + readonly onDidEncounterLanguage: Event = Event.None; + + async createGrammar(modeId: string): Promise { return null; } + startDebugMode(printFn: (str: string) => void, onStop: () => void): void { } +} + +registerSingleton(ITextMateService, SimpleTextMateService); + +//#endregion + + +//#region Accessibility + +class SimpleAccessibilityService implements IAccessibilityService { + + declare readonly _serviceBrand: undefined; + + onDidChangeScreenReaderOptimized = Event.None; + + isScreenReaderOptimized(): boolean { return false; } + async alwaysUnderlineAccessKeys(): Promise { return false; } + setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void { } + getAccessibilitySupport(): AccessibilitySupport { return AccessibilitySupport.Unknown; } +} + +registerSingleton(IAccessibilityService, SimpleAccessibilityService); + +//#endregion + + +//#region Tunnel + +class SimpleTunnelService implements ITunnelService { + + declare readonly _serviceBrand: undefined; + + tunnels: Promise = Promise.resolve([]); + + onTunnelOpened = Event.None; + onTunnelClosed = Event.None; + + openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number): Promise | undefined { return undefined; } + async closeTunnel(remoteHost: string, remotePort: number): Promise { } + setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable { return Disposable.None; } +} + +registerSingleton(ITunnelService, SimpleTunnelService); + +//#endregion + + +//#region User Data Sync + +class SimpleUserDataSyncService implements IUserDataSyncService { + + declare readonly _serviceBrand: undefined; + + onDidChangeStatus = Event.None; + onDidChangeConflicts = Event.None; + onDidChangeLocal = Event.None; + onSyncErrors = Event.None; + onDidChangeLastSyncTime = Event.None; + onDidResetRemote = Event.None; + onDidResetLocal = Event.None; + + status: SyncStatus = SyncStatus.Idle; + conflicts: [SyncResource, IResourcePreview[]][] = []; + lastSyncTime = undefined; + + createSyncTask(): Promise { throw new Error('Method not implemented.'); } + createManualSyncTask(): Promise { throw new Error('Method not implemented.'); } + + async replace(uri: URI): Promise { } + async reset(): Promise { } + async resetRemote(): Promise { } + async resetLocal(): Promise { } + async hasLocalData(): Promise { return false; } + async hasPreviouslySynced(): Promise { return false; } + async resolveContent(resource: URI): Promise { return null; } + async accept(resource: SyncResource, conflictResource: URI, content: string | null | undefined, apply: boolean): Promise { } + async getLocalSyncResourceHandles(resource: SyncResource): Promise { return []; } + async getRemoteSyncResourceHandles(resource: SyncResource): Promise { return []; } + async getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI; }[]> { return []; } + async getMachineId(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise { return undefined; } +} + +registerSingleton(IUserDataSyncService, SimpleUserDataSyncService); + +//#endregion + + +//#region User Data Sync Account + +class SimpleUserDataSyncAccountService implements IUserDataSyncAccountService { + + declare readonly _serviceBrand: undefined; + + onTokenFailed = Event.None; + onDidChangeAccount = Event.None; + + account: IUserDataSyncAccount | undefined = undefined; + + async updateAccount(account: IUserDataSyncAccount | undefined): Promise { } +} + +registerSingleton(IUserDataSyncAccountService, SimpleUserDataSyncAccountService); + +//#endregion + + +//#region User Data Auto Sync Account + +class SimpleUserDataAutoSyncAccountService implements IUserDataAutoSyncService { + + declare readonly _serviceBrand: undefined; + + onError = Event.None; + onDidChangeEnablement = Event.None; + + isEnabled(): boolean { return false; } + canToggleEnablement(): boolean { return false; } + async turnOn(): Promise { } + async turnOff(everywhere: boolean): Promise { } + async triggerSync(sources: string[], hasToLimitSync: boolean, disableCache: boolean): Promise { } +} + +registerSingleton(IUserDataAutoSyncService, SimpleUserDataAutoSyncAccountService); + +//#endregion + + +//#region User Data Sync Store Management + +class SimpleIUserDataSyncStoreManagementService implements IUserDataSyncStoreManagementService { + + declare readonly _serviceBrand: undefined; + + userDataSyncStore: IUserDataSyncStore | undefined = undefined; + + async switch(type: UserDataSyncStoreType): Promise { } + + async getPreviousUserDataSyncStore(): Promise { return undefined; } +} + +registerSingleton(IUserDataSyncStoreManagementService, SimpleIUserDataSyncStoreManagementService); + +//#endregion + + +//#region Timer + +class SimpleTimerService extends AbstractTimerService { + protected _isInitialStartup(): boolean { return true; } + protected _didUseCachedData(): boolean { return false; } + protected async _getWindowCount(): Promise { return 1; } + protected async _extendStartupInfo(info: Writeable): Promise { } +} + +registerSingleton(ITimerService, SimpleTimerService); + +//#endregion + + +//#region Workspace Editing + +class SimpleWorkspaceEditingService implements IWorkspaceEditingService { + + declare readonly _serviceBrand: undefined; + + async addFolders(folders: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): Promise { } + async removeFolders(folders: URI[], donotNotifyError?: boolean): Promise { } + async updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): Promise { } + async enterWorkspace(path: URI): Promise { } + async createAndEnterWorkspace(folders: IWorkspaceFolderCreationData[], path?: URI): Promise { } + async saveAndEnterWorkspace(path: URI): Promise { } + async copyWorkspaceSettings(toWorkspace: IWorkspaceIdentifier): Promise { } + async pickNewWorkspacePath(): Promise { return undefined!; } +} + +registerSingleton(IWorkspaceEditingService, SimpleWorkspaceEditingService); + +//#endregion + + +//#region Task + +class SimpleTaskService implements ITaskService { + + declare readonly _serviceBrand: undefined; + + onDidStateChange = Event.None; + supportsMultipleTaskExecutions = false; + + configureAction(): Action { throw new Error('Method not implemented.'); } + build(): Promise { throw new Error('Method not implemented.'); } + runTest(): Promise { throw new Error('Method not implemented.'); } + run(task: CustomTask | ContributedTask | InMemoryTask | undefined, options?: ProblemMatcherRunOptions): Promise { throw new Error('Method not implemented.'); } + inTerminal(): boolean { throw new Error('Method not implemented.'); } + isActive(): Promise { throw new Error('Method not implemented.'); } + getActiveTasks(): Promise { throw new Error('Method not implemented.'); } + getBusyTasks(): Promise { throw new Error('Method not implemented.'); } + restart(task: Task): void { throw new Error('Method not implemented.'); } + terminate(task: Task): Promise { throw new Error('Method not implemented.'); } + terminateAll(): Promise { throw new Error('Method not implemented.'); } + tasks(filter?: TaskFilter): Promise { throw new Error('Method not implemented.'); } + taskTypes(): string[] { throw new Error('Method not implemented.'); } + getWorkspaceTasks(runSource?: TaskRunSource): Promise> { throw new Error('Method not implemented.'); } + readRecentTasks(): Promise<(CustomTask | ContributedTask | InMemoryTask | ConfiguringTask)[]> { throw new Error('Method not implemented.'); } + getTask(workspaceFolder: string | IWorkspace | IWorkspaceFolder, alias: string | TaskIdentifier, compareId?: boolean): Promise { throw new Error('Method not implemented.'); } + tryResolveTask(configuringTask: ConfiguringTask): Promise { throw new Error('Method not implemented.'); } + getTasksForGroup(group: string): Promise { throw new Error('Method not implemented.'); } + getRecentlyUsedTasks(): LinkedMap { throw new Error('Method not implemented.'); } + migrateRecentTasks(tasks: Task[]): Promise { throw new Error('Method not implemented.'); } + createSorter(): TaskSorter { throw new Error('Method not implemented.'); } + getTaskDescription(task: CustomTask | ContributedTask | InMemoryTask | ConfiguringTask): string | undefined { throw new Error('Method not implemented.'); } + canCustomize(task: CustomTask | ContributedTask): boolean { throw new Error('Method not implemented.'); } + customize(task: CustomTask | ContributedTask | ConfiguringTask, properties?: {}, openConfig?: boolean): Promise { throw new Error('Method not implemented.'); } + openConfig(task: CustomTask | ConfiguringTask | undefined): Promise { throw new Error('Method not implemented.'); } + registerTaskProvider(taskProvider: ITaskProvider, type: string): IDisposable { throw new Error('Method not implemented.'); } + registerTaskSystem(scheme: string, taskSystemInfo: TaskSystemInfo): void { throw new Error('Method not implemented.'); } + registerSupportedExecutions(custom?: boolean, shell?: boolean, process?: boolean): void { throw new Error('Method not implemented.'); } + setJsonTasksSupported(areSuppored: Promise): void { throw new Error('Method not implemented.'); } + extensionCallbackTaskComplete(task: Task, result: number | undefined): Promise { throw new Error('Method not implemented.'); } +} + +registerSingleton(ITaskService, SimpleTaskService); + +//#endregion + + +//#region Extension Management + +class SimpleExtensionManagementService implements IExtensionManagementService { + + declare readonly _serviceBrand: undefined; + + onInstallExtension = Event.None; + onDidInstallExtension = Event.None; + onUninstallExtension = Event.None; + onDidUninstallExtension = Event.None; + + async zip(extension: ILocalExtension): Promise { throw new Error('Method not implemented.'); } + async unzip(zipLocation: URI): Promise { throw new Error('Method not implemented.'); } + async getManifest(vsix: URI): Promise { throw new Error('Method not implemented.'); } + async install(vsix: URI, isMachineScoped?: boolean): Promise { throw new Error('Method not implemented.'); } + async canInstall(extension: IGalleryExtension): Promise { throw new Error('Method not implemented.'); } + async installFromGallery(extension: IGalleryExtension, isMachineScoped?: boolean): Promise { throw new Error('Method not implemented.'); } + async uninstall(extension: ILocalExtension, force?: boolean): Promise { } + async reinstallFromGallery(extension: ILocalExtension): Promise { } + async getInstalled(type?: ExtensionType): Promise { return []; } + async getExtensionsReport(): Promise { return []; } + async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise { throw new Error('Method not implemented.'); } +} + +registerSingleton(IExtensionManagementService, SimpleExtensionManagementService); + +//#endregion + + +//#region Extension Tips + +class SimpleExtensionTipsService implements IExtensionTipsService { + + declare readonly _serviceBrand: undefined; + + onRecommendationChange = Event.None; + + getAllRecommendationsWithReason(): { [id: string]: { reasonId: ExtensionRecommendationReason; reasonText: string; }; } { return Object.create(null); } + getFileBasedRecommendations(): IExtensionRecommendation[] { return []; } + async getOtherRecommendations(): Promise { return []; } + async getWorkspaceRecommendations(): Promise { return []; } + getKeymapRecommendations(): IExtensionRecommendation[] { return []; } + toggleIgnoredRecommendation(extensionId: string, shouldIgnore: boolean): void { } + getAllIgnoredRecommendations(): { global: string[]; workspace: string[]; } { return Object.create(null); } + async getConfigBasedTips(folder: URI): Promise { return []; } + async getImportantExecutableBasedTips(): Promise { return []; } + async getOtherExecutableBasedTips(): Promise { return []; } + async getAllWorkspacesTips(): Promise { return []; } +} + +registerSingleton(IExtensionTipsService, SimpleExtensionTipsService); + +//#endregion + + +//#region Workspace Tags + +class SimpleWorkspaceTagsService implements IWorkspaceTagsService { + + declare readonly _serviceBrand: undefined; + + async getTags(): Promise { return Object.create(null); } + getTelemetryWorkspaceId(workspace: IWorkspace, state: WorkbenchState): string | undefined { return undefined; } + async getHashedRemotesFromUri(workspaceUri: URI, stripEndingDotGit?: boolean): Promise { return []; } +} + +registerSingleton(IWorkspaceTagsService, SimpleWorkspaceTagsService); + +//#endregion + + +//#region Output Channel + +class SimpleOutputChannelModelService extends AsbtractOutputChannelModelService { + declare readonly _serviceBrand: undefined; +} + +registerSingleton(IOutputChannelModelService, SimpleOutputChannelModelService); + +//#endregion diff --git a/src/vs/workbench/services/accessibility/electron-browser/accessibilityService.ts b/src/vs/workbench/services/accessibility/electron-browser/accessibilityService.ts index 5834d7e9023..166a53e48e1 100644 --- a/src/vs/workbench/services/accessibility/electron-browser/accessibilityService.ts +++ b/src/vs/workbench/services/accessibility/electron-browser/accessibilityService.ts @@ -41,23 +41,21 @@ export class NativeAccessibilityService extends AccessibilityService implements this.setAccessibilitySupport(environmentService.configuration.accessibilitySupport ? AccessibilitySupport.Enabled : AccessibilitySupport.Disabled); } - alwaysUnderlineAccessKeys(): Promise { + async alwaysUnderlineAccessKeys(): Promise { if (!isWindows) { - return Promise.resolve(false); + return false; } - return new Promise(async (resolve) => { - const Registry = await import('vscode-windows-registry'); + const Registry = await import('vscode-windows-registry'); - let value; - try { - value = Registry.GetStringRegKey('HKEY_CURRENT_USER', 'Control Panel\\Accessibility\\Keyboard Preference', 'On'); - } catch { - resolve(false); - } + let value: string | undefined = undefined; + try { + value = Registry.GetStringRegKey('HKEY_CURRENT_USER', 'Control Panel\\Accessibility\\Keyboard Preference', 'On'); + } catch { + return false; + } - resolve(value === '1'); - }); + return value === '1'; } setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void { diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 9741fd8fbcf..5b609209058 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -16,9 +16,34 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { isString } from 'vs/base/common/types'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { flatten } from 'vs/base/common/arrays'; +import { isFalsyOrWhitespace } from 'vs/base/common/strings'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export function getAuthenticationProviderActivationEvent(id: string): string { return `onAuthenticationRequest:${id}`; } +export type AuthenticationSessionInfo = { readonly id: string, readonly accessToken: string, readonly providerId: string }; +export async function getCurrentAuthenticationSessionInfo(environmentService: IWorkbenchEnvironmentService, productService: IProductService): Promise { + if (environmentService.options?.credentialsProvider) { + const authenticationSessionValue = await environmentService.options.credentialsProvider.getPassword(`${productService.urlProtocol}.login`, 'account'); + if (authenticationSessionValue) { + const authenticationSessionInfo: AuthenticationSessionInfo = JSON.parse(authenticationSessionValue); + if (authenticationSessionInfo + && isString(authenticationSessionInfo.id) + && isString(authenticationSessionInfo.accessToken) + && isString(authenticationSessionInfo.providerId) + ) { + return authenticationSessionInfo; + } + } + } + return undefined; +} + export const IAuthenticationService = createDecorator('IAuthenticationService'); export interface IAuthenticationService { @@ -35,6 +60,10 @@ export interface IAuthenticationService { readonly onDidUnregisterAuthenticationProvider: Event; readonly onDidChangeSessions: Event<{ providerId: string, label: string, event: AuthenticationSessionsChangeEvent }>; + + declaredProviders: AuthenticationProviderInformation[]; + readonly onDidChangeDeclaredProviders: Event; + getSessions(providerId: string): Promise>; getLabel(providerId: string): string; supportsMultipleAccounts(providerId: string): boolean; @@ -76,6 +105,30 @@ CommandsRegistry.registerCommand('workbench.getCodeExchangeProxyEndpoints', func return environmentService.options?.codeExchangeProxyEndpoints; }); +const authenticationDefinitionSchema: IJSONSchema = { + type: 'object', + additionalProperties: false, + properties: { + id: { + type: 'string', + description: nls.localize('authentication.id', 'The id of the authentication provider.') + }, + label: { + type: 'string', + description: nls.localize('authentication.label', 'The human readable name of the authentication provider.'), + } + } +}; + +const authenticationExtPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'authentication', + jsonSchema: { + description: nls.localize('authenticationExtensionPoint', 'Contributes authentication'), + type: 'array', + items: authenticationDefinitionSchema + } +}); + export class AuthenticationService extends Disposable implements IAuthenticationService { declare readonly _serviceBrand: undefined; private _placeholderMenuItem: IDisposable | undefined; @@ -85,6 +138,11 @@ export class AuthenticationService extends Disposable implements IAuthentication private _authenticationProviders: Map = new Map(); + /** + * All providers that have been statically declared by extensions. These may not be registered. + */ + declaredProviders: AuthenticationProviderInformation[] = []; + private _onDidRegisterAuthenticationProvider: Emitter = this._register(new Emitter()); readonly onDidRegisterAuthenticationProvider: Event = this._onDidRegisterAuthenticationProvider.event; @@ -94,7 +152,13 @@ export class AuthenticationService extends Disposable implements IAuthentication private _onDidChangeSessions: Emitter<{ providerId: string, label: string, event: AuthenticationSessionsChangeEvent }> = this._register(new Emitter<{ providerId: string, label: string, event: AuthenticationSessionsChangeEvent }>()); readonly onDidChangeSessions: Event<{ providerId: string, label: string, event: AuthenticationSessionsChangeEvent }> = this._onDidChangeSessions.event; - constructor(@IActivityService private readonly activityService: IActivityService) { + private _onDidChangeDeclaredProviders: Emitter = this._register(new Emitter()); + readonly onDidChangeDeclaredProviders: Event = this._onDidChangeDeclaredProviders.event; + + constructor( + @IActivityService private readonly activityService: IActivityService, + @IExtensionService private readonly extensionService: IExtensionService + ) { super(); this._placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { command: { @@ -103,6 +167,38 @@ export class AuthenticationService extends Disposable implements IAuthentication precondition: ContextKeyExpr.false() }, }); + + authenticationExtPoint.setHandler((extensions, { added, removed }) => { + added.forEach(point => { + for (const provider of point.value) { + if (isFalsyOrWhitespace(provider.id)) { + point.collector.error(nls.localize('authentication.missingId', 'An authentication contribution must specify an id.')); + continue; + } + + if (isFalsyOrWhitespace(provider.label)) { + point.collector.error(nls.localize('authentication.missingLabel', 'An authentication contribution must specify a label.')); + continue; + } + + if (!this.declaredProviders.some(p => p.id === provider.id)) { + this.declaredProviders.push(provider); + } else { + point.collector.error(nls.localize('authentication.idConflict', "This authentication id '{0}' has already been registered", provider.id)); + } + } + }); + + const removedExtPoints = flatten(removed.map(r => r.value)); + removedExtPoints.forEach(point => { + const index = this.declaredProviders.findIndex(provider => provider.id === point.id); + if (index > -1) { + this.declaredProviders.splice(index, 1); + } + }); + + this._onDidChangeDeclaredProviders.fire(this.declaredProviders); + }); } getProviderIds(): string[] { @@ -319,11 +415,11 @@ export class AuthenticationService extends Disposable implements IAuthentication } } getLabel(id: string): string { - const authProvider = this._authenticationProviders.get(id); + const authProvider = this.declaredProviders.find(provider => provider.id === id); if (authProvider) { return authProvider.label; } else { - throw new Error(`No authentication provider '${id}' is currently registered.`); + throw new Error(`No authentication provider '${id}' has been declared.`); } } @@ -337,6 +433,8 @@ export class AuthenticationService extends Disposable implements IAuthentication } async getSessions(id: string): Promise> { + await this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(id)); + const authProvider = this._authenticationProviders.get(id); if (authProvider) { return await authProvider.getSessions(); @@ -346,6 +444,8 @@ export class AuthenticationService extends Disposable implements IAuthentication } async login(id: string, scopes: string[]): Promise { + await this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(id)); + const authProvider = this._authenticationProviders.get(id); if (authProvider) { return authProvider.login(scopes); diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index 747f5506106..650b4c9fad3 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -22,12 +22,14 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ConfigurationEditingService, EditableConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditingService'; import { WorkspaceConfiguration, FolderConfiguration, RemoteUserConfiguration, UserConfiguration } from 'vs/workbench/services/configuration/browser/configuration'; import { JSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditingService'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { isEqual, dirname } from 'vs/base/common/resources'; import { mark } from 'vs/base/common/performance'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IFileService } from 'vs/platform/files/common/files'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; export class WorkspaceService extends Disposable implements IConfigurationService, IWorkspaceContextService { @@ -73,6 +75,13 @@ export class WorkspaceService extends Disposable implements IConfigurationServic ) { super(); + const configurationRegistry = Registry.as(Extensions.Configuration); + // register defaults before creating default configuration model + // so that the model is not required to be updated after registering + if (environmentService.options?.configurationDefaults) { + configurationRegistry.registerDefaultConfigurations([environmentService.options.configurationDefaults]); + } + this.completeWorkspaceBarrier = new Barrier(); this.defaultConfiguration = new DefaultConfigurationModel(); this.configurationCache = configurationCache; @@ -94,11 +103,6 @@ export class WorkspaceService extends Disposable implements IConfigurationServic }); })); - const configurationRegistry = Registry.as(Extensions.Configuration); - if (environmentService.options?.configurationDefaults) { - configurationRegistry.registerDefaultConfigurations([environmentService.options.configurationDefaults]); - } - this._register(configurationRegistry.onDidSchemaChange(e => this.registerConfigurationSchemas())); this._register(configurationRegistry.onDidUpdateConfiguration(configurationProperties => this.onDefaultConfigurationChanged(configurationProperties))); this.workspaceEditingQueue = new Queue(); @@ -423,7 +427,6 @@ export class WorkspaceService extends Disposable implements IConfigurationServic } private initializeConfiguration(): Promise { - this.registerConfigurationSchemas(); return this.initializeUserConfiguration() .then(({ local, remote }) => this.loadConfiguration(local, remote)); } @@ -498,7 +501,6 @@ export class WorkspaceService extends Disposable implements IConfigurationServic private onDefaultConfigurationChanged(keys: string[]): void { this.defaultConfiguration = new DefaultConfigurationModel(); - this.registerConfigurationSchemas(); if (this.workspace) { const previousData = this._configuration.toData(); const change = this._configuration.compareAndUpdateDefaultConfiguration(this.defaultConfiguration, keys); @@ -525,30 +527,6 @@ export class WorkspaceService extends Disposable implements IConfigurationServic } } - private registerConfigurationSchemas(): void { - if (this.workspace) { - const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); - const defaultSettingsSchema: IJSONSchema = { additionalProperties: true, allowTrailingCommas: true, allowComments: true }; - const allSettingsSchema: IJSONSchema = { properties: allSettings.properties, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; - const userSettingsSchema: IJSONSchema = this.remoteUserConfiguration ? { properties: { ...applicationSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true } : allSettingsSchema; - const machineSettingsSchema: IJSONSchema = { properties: { ...machineSettings.properties, ...machineOverridableSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; - const workspaceSettingsSchema: IJSONSchema = { properties: { ...machineOverridableSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; - - jsonRegistry.registerSchema(defaultSettingsSchemaId, defaultSettingsSchema); - jsonRegistry.registerSchema(userSettingsSchemaId, userSettingsSchema); - jsonRegistry.registerSchema(machineSettingsSchemaId, machineSettingsSchema); - - if (WorkbenchState.WORKSPACE === this.getWorkbenchState()) { - const folderSettingsSchema: IJSONSchema = { properties: { ...machineOverridableSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; - jsonRegistry.registerSchema(workspaceSettingsSchemaId, workspaceSettingsSchema); - jsonRegistry.registerSchema(folderSettingsSchemaId, folderSettingsSchema); - } else { - jsonRegistry.registerSchema(workspaceSettingsSchemaId, workspaceSettingsSchema); - jsonRegistry.registerSchema(folderSettingsSchemaId, workspaceSettingsSchema); - } - } - } - private onLocalUserConfigurationChanged(userConfiguration: ConfigurationModel): void { const previous = { data: this._configuration.toData(), workspace: this.workspace }; const change = this._configuration.compareAndUpdateLocalUserConfiguration(userConfiguration); @@ -774,3 +752,45 @@ export class WorkspaceService extends Disposable implements IConfigurationServic return null; } } + +class RegisterConfigurationSchemasContribution extends Disposable implements IWorkbenchContribution { + constructor( + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, + ) { + super(); + this.registerConfigurationSchemas(); + const configurationRegistry = Registry.as(Extensions.Configuration); + this._register(configurationRegistry.onDidUpdateConfiguration(e => this.registerConfigurationSchemas())); + this._register(configurationRegistry.onDidSchemaChange(e => this.registerConfigurationSchemas())); + } + + private registerConfigurationSchemas(): void { + const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); + const allSettingsSchema: IJSONSchema = { properties: allSettings.properties, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; + const userSettingsSchema: IJSONSchema = this.workbenchEnvironmentService.configuration.remoteAuthority ? { properties: { ...applicationSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true } : allSettingsSchema; + const machineSettingsSchema: IJSONSchema = { properties: { ...machineSettings.properties, ...machineOverridableSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; + const workspaceSettingsSchema: IJSONSchema = { properties: { ...machineOverridableSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; + + jsonRegistry.registerSchema(defaultSettingsSchemaId, { + properties: Object.keys(allSettings.properties).reduce((result, key) => { result[key] = { ...allSettings.properties[key], deprecationMessage: undefined }; return result; }, {}), + patternProperties: Object.keys(allSettings.patternProperties).reduce((result, key) => { result[key] = { ...allSettings.patternProperties[key], deprecationMessage: undefined }; return result; }, {}), + additionalProperties: true, + allowTrailingCommas: true, + allowComments: true + }); + jsonRegistry.registerSchema(userSettingsSchemaId, userSettingsSchema); + jsonRegistry.registerSchema(machineSettingsSchemaId, machineSettingsSchema); + + if (WorkbenchState.WORKSPACE === this.workspaceContextService.getWorkbenchState()) { + const folderSettingsSchema: IJSONSchema = { properties: { ...machineOverridableSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; + jsonRegistry.registerSchema(workspaceSettingsSchemaId, workspaceSettingsSchema); + jsonRegistry.registerSchema(folderSettingsSchemaId, folderSettingsSchema); + } else { + jsonRegistry.registerSchema(workspaceSettingsSchemaId, workspaceSettingsSchema); + jsonRegistry.registerSchema(folderSettingsSchemaId, workspaceSettingsSchema); + } + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(RegisterConfigurationSchemasContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/services/configuration/common/configurationEditingService.ts b/src/vs/workbench/services/configuration/common/configurationEditingService.ts index 37ef711911b..d748536584c 100644 --- a/src/vs/workbench/services/configuration/common/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/common/configurationEditingService.ts @@ -353,8 +353,9 @@ export class ConfigurationEditingService { } } return nls.localize('errorInvalidConfigurationFolder', "Unable to write into folder settings. Please open the '{0}' folder settings to correct errors/warnings in it and try again.", workspaceFolderName); + default: + return ''; } - return ''; } case ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY: { if (operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY) { @@ -379,8 +380,9 @@ export class ConfigurationEditingService { } } return nls.localize('errorConfigurationFileDirtyFolder', "Unable to write into folder settings because the file is dirty. Please save the '{0}' folder settings file first and then try again.", workspaceFolderName); + default: + return ''; } - return ''; } case ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_MODIFIED_SINCE: if (operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY) { @@ -412,8 +414,9 @@ export class ConfigurationEditingService { return nls.localize('workspaceTarget', "Workspace Settings"); case EditableConfigurationTarget.WORKSPACE_FOLDER: return nls.localize('folderTarget', "Folder Settings"); + default: + return ''; } - return ''; } private getEdits(model: ITextModel, edit: IConfigurationEditOperation): Edit[] { diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts index 5091446162d..6d2f43ace73 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts @@ -39,6 +39,7 @@ import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService } from 'vs/platform/log/common/log'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { ConfigurationCache } from 'vs/workbench/services/configuration/node/configurationCache'; +import { ConfigurationCache as BrowserConfigurationCache } from 'vs/workbench/services/configuration/browser/configurationCache'; import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { IConfigurationCache } from 'vs/workbench/services/configuration/common/configuration'; import { SignService } from 'vs/platform/sign/browser/signService'; @@ -50,6 +51,7 @@ import { timeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { DisposableStore } from 'vs/base/common/lifecycle'; import product from 'vs/platform/product/common/product'; +import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; class TestEnvironmentService extends NativeWorkbenchEnvironmentService { @@ -2059,6 +2061,46 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { }); +suite('ConfigurationService - Configuration Defaults', () => { + + const disposableStore: DisposableStore = new DisposableStore(); + + suiteSetup(() => { + Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ + 'id': '_test', + 'type': 'object', + 'properties': { + 'configurationService.defaultOverridesSetting': { + 'type': 'string', + 'default': 'isSet', + }, + } + }); + }); + + teardown(() => { + disposableStore.clear(); + }); + + test('when default value is not overriden', () => { + const testObject = createConfiurationService({}); + assert.deepEqual(testObject.getValue('configurationService.defaultOverridesSetting'), 'isSet'); + }); + + test('when default value is overriden', () => { + const testObject = createConfiurationService({ 'configurationService.defaultOverridesSetting': 'overriddenValue' }); + assert.deepEqual(testObject.getValue('configurationService.defaultOverridesSetting'), 'overriddenValue'); + }); + + function createConfiurationService(configurationDefaults: Record): IConfigurationService { + const remoteAgentService = (workbenchInstantiationService()).createInstance(RemoteAgentService); + const environmentService = new BrowserWorkbenchEnvironmentService({ logsPath: URI.file(''), workspaceId: '', configurationDefaults }); + const fileService = new FileService(new NullLogService()); + return disposableStore.add(new WorkspaceService({ configurationCache: new BrowserConfigurationCache() }, environmentService, fileService, remoteAgentService)); + } + +}); + function getWorkspaceId(configPath: URI): string { let workspaceConfigPath = configPath.scheme === Schemas.file ? originalFSPath(configPath) : configPath.toString(); if (!isLinux) { diff --git a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts index 42b3431dc41..7fc15ac3615 100644 --- a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts @@ -264,7 +264,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR if (!Types.isString(info.description)) { missingAttribute('description'); } - const inputOptions: IInputOptions = { prompt: info.description }; + const inputOptions: IInputOptions = { prompt: info.description, ignoreFocusLost: true }; if (info.default) { inputOptions.value = info.default; } @@ -310,7 +310,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR picks.push(item); } }); - const pickOptions: IPickOptions = { placeHolder: info.description, matchOnDetail: true }; + const pickOptions: IPickOptions = { placeHolder: info.description, matchOnDetail: true, ignoreFocusLost: true }; return this.quickInputService.pick(picks, pickOptions, undefined).then(resolvedInput => { if (resolvedInput) { return resolvedInput.value; diff --git a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts index ee78a258ee3..861d178a11b 100644 --- a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts +++ b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts @@ -142,10 +142,9 @@ class NativeContextMenuService extends Disposable implements IContextMenuService return undefined; } - const actions = Array.isArray(entry.actions) ? entry.actions : entry.actions(); return { label: unmnemonicLabel(stripCodicons(entry.label)).trim(), - submenu: this.createMenu(delegate, actions, onHide, new Set([...submenuIds, entry.id])) + submenu: this.createMenu(delegate, entry.actions, onHide, new Set([...submenuIds, entry.id])) }; } diff --git a/src/vs/workbench/services/credentials/browser/credentialsService.ts b/src/vs/workbench/services/credentials/browser/credentialsService.ts index 5d64ecf1d2f..c95b04b5ad1 100644 --- a/src/vs/workbench/services/credentials/browser/credentialsService.ts +++ b/src/vs/workbench/services/credentials/browser/credentialsService.ts @@ -3,21 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; +import { ICredentialsProvider, ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { find } from 'vs/base/common/arrays'; -export interface ICredentialsProvider { - getPassword(service: string, account: string): Promise; - setPassword(service: string, account: string, password: string): Promise; - - deletePassword(service: string, account: string): Promise; - - findPassword(service: string): Promise; - findCredentials(service: string): Promise>; -} - export class BrowserCredentialsService implements ICredentialsService { declare readonly _serviceBrand: undefined; diff --git a/src/vs/workbench/services/dialogs/browser/dialogService.ts b/src/vs/workbench/services/dialogs/browser/dialogService.ts index 6e3182a696d..1360c248eb7 100644 --- a/src/vs/workbench/services/dialogs/browser/dialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/dialogService.ts @@ -24,7 +24,7 @@ export class DialogService implements IDialogService { declare readonly _serviceBrand: undefined; - private allowableCommands = ['copy', 'cut', 'editor.action.clipboardCopyAction', 'editor.action.clipboardCopyAction']; + private allowableCommands = ['copy', 'cut', 'editor.action.clipboardCopyAction', 'editor.action.clipboardCutAction']; constructor( @ILogService private readonly logService: ILogService, diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 88800a2c196..2f64b62a888 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -747,9 +747,9 @@ export class EditorService extends Disposable implements EditorServiceImpl { //#region replaceEditors() - replaceEditors(editors: IResourceEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise; - replaceEditors(editors: IEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise; - replaceEditors(editors: Array, group: IEditorGroup | GroupIdentifier): Promise { + async replaceEditors(editors: IResourceEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise; + async replaceEditors(editors: IEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise; + async replaceEditors(editors: Array, group: IEditorGroup | GroupIdentifier): Promise { const typedEditors: IEditorReplacement[] = []; editors.forEach(replaceEditorArg => { @@ -776,8 +776,6 @@ export class EditorService extends Disposable implements EditorServiceImpl { if (targetGroup) { return targetGroup.replaceEditors(typedEditors); } - - return Promise.resolve(); } //#endregion diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index bec876bc4d3..817616f1ccd 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { EditorActivation } from 'vs/platform/editor/common/editor'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { EditorInput, EditorsOrder, SideBySideEditorInput } from 'vs/workbench/common/editor'; import { workbenchInstantiationService, TestServiceAccessor, registerTestEditor, TestFileEditorInput } from 'vs/workbench/test/browser/workbenchTestServices'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; @@ -386,7 +386,7 @@ suite('EditorService', () => { test('delegate', function (done) { const instantiationService = workbenchInstantiationService(); - class MyEditor extends BaseEditor { + class MyEditor extends EditorPane { constructor(id: string) { super(id, undefined!, new TestThemeService(), new TestStorageService()); diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index ba2701ec54d..819607be0c1 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -14,7 +14,6 @@ import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api'; import product from 'vs/platform/product/common/product'; import { memoize } from 'vs/base/common/decorators'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { LIGHT } from 'vs/platform/theme/common/themeService'; import { parseLineAndColumnAware } from 'vs/base/common/extpath'; export class BrowserEnvironmentConfiguration implements IEnvironmentConfiguration { @@ -78,10 +77,6 @@ export class BrowserEnvironmentConfiguration implements IEnvironmentConfiguratio get highContrast() { return false; // could investigate to detect high contrast theme automatically } - - get defaultThemeType() { - return LIGHT; - } } interface IBrowserWorkbenchEnvironmentConstructionOptions extends IWorkbenchConstructionOptions { diff --git a/src/vs/workbench/services/environment/electron-browser/environmentService.ts b/src/vs/workbench/services/environment/electron-browser/environmentService.ts index 20da095a459..46f084aff4c 100644 --- a/src/vs/workbench/services/environment/electron-browser/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-browser/environmentService.ts @@ -17,8 +17,8 @@ export interface INativeWorkbenchEnvironmentService extends IWorkbenchEnvironmen readonly configuration: INativeEnvironmentConfiguration; - readonly disableCrashReporter: boolean; readonly crashReporterDirectory?: string; + readonly crashReporterId?: string; readonly cliPath: string; diff --git a/src/vs/workbench/services/experiment/electron-browser/experimentService.ts b/src/vs/workbench/services/experiment/electron-browser/experimentService.ts index 966ea7736f8..b9af68bd937 100644 --- a/src/vs/workbench/services/experiment/electron-browser/experimentService.ts +++ b/src/vs/workbench/services/experiment/electron-browser/experimentService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as platform from 'vs/base/common/platform'; -import { IKeyValueStorage, IExperimentationTelemetry, IExperimentationFilterProvider, ExperimentationService as TASClient } from 'tas-client'; +import type { IKeyValueStorage, IExperimentationTelemetry, IExperimentationFilterProvider, ExperimentationService as TASClient } from 'tas-client'; import { MementoObject, Memento } from 'vs/workbench/common/memento'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -12,9 +12,10 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { ITelemetryData } from 'vs/base/common/actions'; import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; const storageKey = 'VSCode.ABExp.FeatureData'; -const refetchInterval = 1000 * 60 * 30; // By default it's set up to 30 minutes. +const refetchInterval = 0; // no polling class MementoKeyValueStorage implements IKeyValueStorage { constructor(private mementoObj: MementoObject) { } @@ -160,13 +161,18 @@ export class ExperimentService implements ITASExperimentService { private tasClient: Promise | undefined; private static MEMENTO_ID = 'experiment.service.memento'; + private get experimentsEnabled(): boolean { + return this.configurationService.getValue('workbench.enableExperiments') === true; + } + constructor( @IProductService private productService: IProductService, @ITelemetryService private telemetryService: ITelemetryService, - @IStorageService private storageService: IStorageService + @IStorageService private storageService: IStorageService, + @IConfigurationService private configurationService: IConfigurationService, ) { - if (this.productService.tasConfig) { + if (this.productService.tasConfig && this.experimentsEnabled && this.telemetryService.isOptedIn) { this.tasClient = this.setupTASClient(); } } @@ -176,6 +182,10 @@ export class ExperimentService implements ITASExperimentService { return undefined; } + if (!this.experimentsEnabled) { + return undefined; + } + return (await this.tasClient).getTreatmentVariable('vscode', name); } @@ -196,7 +206,7 @@ export class ExperimentService implements ITASExperimentService { const telemetry = new ExperimentServiceTelemetry(this.telemetryService); const tasConfig = this.productService.tasConfig!; - const tasClient = new TASClient({ + const tasClient = new (await import('tas-client')).ExperimentationService({ filterProviders: [filterProvider], telemetry: telemetry, storageKey: storageKey, diff --git a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts index c28b1477400..32f3dc52c1f 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts @@ -12,13 +12,15 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { ExtensionType, IExtension } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, IExtension, isAuthenticaionProviderExtension, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { getExtensionKind } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IProductService } from 'vs/platform/product/common/productService'; import { StorageManager } from 'vs/platform/extensionManagement/common/extensionEnablementService'; import { webWorkerExtHostConfig } from 'vs/workbench/services/extensions/common/extensions'; +import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; +import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; const SOURCE = 'IWorkbenchExtensionEnablementService'; @@ -40,6 +42,8 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IProductService private readonly productService: IProductService, + @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, + @IUserDataSyncAccountService private readonly userDataSyncAccountService: IUserDataSyncAccountService, ) { super(); this.storageManger = this._register(new StorageManager(storageService)); @@ -66,7 +70,9 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } canChangeEnablement(extension: IExtension): boolean { - if (extension.manifest && extension.manifest.contributes && extension.manifest.contributes.localizations && extension.manifest.contributes.localizations.length) { + try { + this.throwErrorIfCannotChangeEnablement(extension); + } catch (error) { return false; } const enablementState = this.getEnablementState(extension); @@ -76,11 +82,47 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return true; } + private throwErrorIfCannotChangeEnablement(extension: IExtension): void { + if (isLanguagePackExtension(extension.manifest)) { + throw new Error(localize('cannot disable language pack extension', "Cannot disable {0} extension because it contributes language packs.", extension.manifest.displayName || extension.identifier.id)); + } + + if (this.userDataAutoSyncService.isEnabled() && this.userDataSyncAccountService.account && + isAuthenticaionProviderExtension(extension.manifest) && extension.manifest.contributes!.authentication!.some(a => a.id === this.userDataSyncAccountService.account!.authenticationProviderId)) { + throw new Error(localize('cannot disable auth extension', "Cannot disable {0} extension because Settings Sync depends on it.", extension.manifest.displayName || extension.identifier.id)); + } + } + + canChangeWorkspaceEnablement(extension: IExtension): boolean { + if (!this.canChangeEnablement(extension)) { + return false; + } + try { + this.throwErrorIfCannotChangeWorkspaceEnablement(extension); + } catch (error) { + return false; + } + return true; + } + + private throwErrorIfCannotChangeWorkspaceEnablement(extension: IExtension): void { + if (!this.hasWorkspace) { + throw new Error(localize('noWorkspace', "No workspace.")); + } + if (isAuthenticaionProviderExtension(extension.manifest)) { + throw new Error(localize('cannot disable auth extension in workspace', "Cannot disable {0} extension in workspace because it contributes authentication providers", extension.manifest.displayName || extension.identifier.id)); + } + } + async setEnablement(extensions: IExtension[], newState: EnablementState): Promise { const workspace = newState === EnablementState.DisabledWorkspace || newState === EnablementState.EnabledWorkspace; - if (workspace && !this.hasWorkspace) { - return Promise.reject(new Error(localize('noWorkspace', "No workspace."))); + for (const extension of extensions) { + if (workspace) { + this.throwErrorIfCannotChangeWorkspaceEnablement(extension); + } else { + this.throwErrorIfCannotChangeEnablement(extension); + } } const result = await Promise.all(extensions.map(e => this._setEnablement(e, newState))); @@ -316,4 +358,4 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } } -registerSingleton(IWorkbenchExtensionEnablementService, ExtensionEnablementService, true); +registerSingleton(IWorkbenchExtensionEnablementService, ExtensionEnablementService); diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 34647803e9b..f18d12e8e8b 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -58,6 +58,11 @@ export interface IWorkbenchExtensionEnablementService { */ canChangeEnablement(extension: IExtension): boolean; + /** + * Returns `true` if the enablement can be changed. + */ + canChangeWorkspaceEnablement(extension: IExtension): boolean; + /** * Returns `true` if the given extension identifier is enabled. */ @@ -127,10 +132,11 @@ export interface IExtensionRecommendationsService { readonly _serviceBrand: undefined; getAllRecommendationsWithReason(): IStringDictionary; - getFileBasedRecommendations(): IExtensionRecommendation[]; getImportantRecommendations(): Promise; - getConfigBasedRecommendations(): Promise; getOtherRecommendations(): Promise; + getFileBasedRecommendations(): IExtensionRecommendation[]; + getExeBasedRecommendations(exe?: string): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }>; + getConfigBasedRecommendations(): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }>; getWorkspaceRecommendations(): Promise; getKeymapRecommendations(): IExtensionRecommendation[]; diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts index 388d6996081..2fa59ee92f4 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts @@ -5,7 +5,6 @@ import { localize } from 'vs/nls'; import { IExtensionManagementServer, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; @@ -15,6 +14,10 @@ import { isWeb } from 'vs/base/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { WebExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/webExtensionManagementService'; import { IExtension } from 'vs/platform/extensions/common/extensions'; +import { WebRemoteExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/remoteExtensionManagementService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IProductService } from 'vs/platform/product/common/productService'; export class ExtensionManagementServerService implements IExtensionManagementServerService { @@ -27,11 +30,14 @@ export class ExtensionManagementServerService implements IExtensionManagementSer constructor( @IRemoteAgentService remoteAgentService: IRemoteAgentService, @ILabelService labelService: ILabelService, + @IExtensionGalleryService galleryService: IExtensionGalleryService, + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService, @IInstantiationService instantiationService: IInstantiationService, ) { const remoteAgentConnection = remoteAgentService.getConnection(); if (remoteAgentConnection) { - const extensionManagementService = new ExtensionManagementChannelClient(remoteAgentConnection!.getChannel('extensions')); + const extensionManagementService = new WebRemoteExtensionManagementService(remoteAgentConnection.getChannel('extensions'), galleryService, configurationService, productService); this.remoteExtensionManagementServer = { id: 'remote', extensionManagementService, diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 33eb56db3c2..a982b3ecc58 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -189,6 +189,15 @@ export class ExtensionManagementService extends Disposable implements IExtension return Promise.reject('No Servers'); } + async canInstall(gallery: IGalleryExtension): Promise { + for (const server of this.servers) { + if (await server.extensionManagementService.canInstall(gallery)) { + return true; + } + } + return false; + } + async installFromGallery(gallery: IGalleryExtension): Promise { // Only local server, install without any checks diff --git a/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts new file mode 100644 index 00000000000..908ce5a2ab1 --- /dev/null +++ b/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.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. + *--------------------------------------------------------------------------------------------*/ + +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IExtensionManagementService, IGalleryExtension, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { canExecuteOnWorkspace } from 'vs/workbench/services/extensions/common/extensionsUtil'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; + +export class WebRemoteExtensionManagementService extends ExtensionManagementChannelClient implements IExtensionManagementService { + + constructor( + channel: IChannel, + @IExtensionGalleryService protected readonly galleryService: IExtensionGalleryService, + @IConfigurationService protected readonly configurationService: IConfigurationService, + @IProductService protected readonly productService: IProductService + ) { + super(channel); + } + + async canInstall(extension: IGalleryExtension): Promise { + const manifest = await this.galleryService.getManifest(extension, CancellationToken.None); + return !!manifest && canExecuteOnWorkspace(manifest, this.productService, this.configurationService); + } + +} diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index 98a423df368..7282f156199 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -11,6 +11,7 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import { IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ILogService } from 'vs/platform/log/common/log'; import { Disposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; export class WebExtensionManagementService extends Disposable implements IExtensionManagementService { @@ -40,7 +41,14 @@ export class WebExtensionManagementService extends Disposable implements IExtens return Promise.all(extensions.map(e => this.toLocalExtension(e))); } + async canInstall(gallery: IGalleryExtension): Promise { + return !!gallery.properties.webExtension; + } + async installFromGallery(gallery: IGalleryExtension): Promise { + if (!(await this.canInstall(gallery))) { + throw new Error(localize('non web extension', "Cannot install because {0} is not a web extension", gallery.displayName)); + } this.logService.info('Installing extension:', gallery.identifier.id); this._onInstallExtension.fire({ identifier: gallery.identifier, gallery }); try { diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts index 2faf33d9c22..2e8d7fa49b0 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as semver from 'semver-umd'; import { IBuiltinExtensionsScannerService, IScannedExtension, ExtensionType, IExtensionIdentifier, ITranslatedScannedExtension } from 'vs/platform/extensions/common/extensions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -225,6 +224,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten } private async scanUserExtensions(): Promise { + const semver = await import('semver-umd'); let userExtensions = await this.readUserExtensions(); const byExtension: IUserExtension[][] = groupByExtension(userExtensions, e => e.identifier); userExtensions = byExtension.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version))[0]); diff --git a/src/vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService.ts b/src/vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService.ts index 0ac506857c7..98538abceb8 100644 --- a/src/vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService.ts @@ -5,7 +5,6 @@ import { localize } from 'vs/nls'; import { Schemas } from 'vs/base/common/network'; -import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManagementServer, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; @@ -13,12 +12,13 @@ import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ILogService } from 'vs/platform/log/common/log'; -import { RemoteExtensionManagementChannelClient } from 'vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IProductService } from 'vs/platform/product/common/productService'; +import { NativeRemoteExtensionManagementService } from 'vs/workbench/services/extensionManagement/electron-browser/remoteExtensionManagementService'; import { ILabelService } from 'vs/platform/label/common/label'; import { IExtension } from 'vs/platform/extensions/common/extensions'; +import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILogService } from 'vs/platform/log/common/log'; export class ExtensionManagementServerService implements IExtensionManagementServerService { @@ -32,18 +32,18 @@ export class ExtensionManagementServerService implements IExtensionManagementSer constructor( @ISharedProcessService sharedProcessService: ISharedProcessService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IExtensionGalleryService galleryService: IExtensionGalleryService, - @IConfigurationService configurationService: IConfigurationService, - @IProductService productService: IProductService, - @ILogService logService: ILogService, @ILabelService labelService: ILabelService, + @IExtensionGalleryService galleryService: IExtensionGalleryService, + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService, + @ILogService logService: ILogService, ) { const localExtensionManagementService = new ExtensionManagementChannelClient(sharedProcessService.getChannel('extensions')); this._localExtensionManagementServer = { extensionManagementService: localExtensionManagementService, id: 'local', label: localize('local', "Local") }; const remoteAgentConnection = remoteAgentService.getConnection(); if (remoteAgentConnection) { - const extensionManagementService = new RemoteExtensionManagementChannelClient(remoteAgentConnection.getChannel('extensions'), this.localExtensionManagementServer.extensionManagementService, galleryService, logService, configurationService, productService); + const extensionManagementService = new NativeRemoteExtensionManagementService(remoteAgentConnection.getChannel('extensions'), this.localExtensionManagementServer, logService, galleryService, configurationService, productService); this.remoteExtensionManagementServer = { id: 'remote', extensionManagementService, diff --git a/src/vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc.ts b/src/vs/workbench/services/extensionManagement/electron-browser/remoteExtensionManagementService.ts similarity index 86% rename from src/vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc.ts rename to src/vs/workbench/services/extensionManagement/electron-browser/remoteExtensionManagementService.ts index b11f3e7f7fa..49d6ec9fbdf 100644 --- a/src/vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc.ts +++ b/src/vs/workbench/services/extensionManagement/electron-browser/remoteExtensionManagementService.ts @@ -12,28 +12,30 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import { ILogService } from 'vs/platform/log/common/log'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { prefersExecuteOnUI } from 'vs/workbench/services/extensions/common/extensionsUtil'; -import { isNonEmptyArray, toArray } from 'vs/base/common/arrays'; +import { isNonEmptyArray } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { localize } from 'vs/nls'; import { IProductService } from 'vs/platform/product/common/productService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { generateUuid } from 'vs/base/common/uuid'; import { joinPath } from 'vs/base/common/resources'; +import { WebRemoteExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/remoteExtensionManagementService'; +import { IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -export class RemoteExtensionManagementChannelClient extends ExtensionManagementChannelClient { +export class NativeRemoteExtensionManagementService extends WebRemoteExtensionManagementService implements IExtensionManagementService { - declare readonly _serviceBrand: undefined; + private readonly localExtensionManagementService: IExtensionManagementService; constructor( channel: IChannel, - private readonly localExtensionManagementService: IExtensionManagementService, - private readonly galleryService: IExtensionGalleryService, - private readonly logService: ILogService, - private readonly configurationService: IConfigurationService, - private readonly productService: IProductService + localExtensionManagementServer: IExtensionManagementServer, + @ILogService private readonly logService: ILogService, + @IExtensionGalleryService galleryService: IExtensionGalleryService, + @IConfigurationService configurationService: IConfigurationService, + @IProductService productService: IProductService ) { - super(channel); + super(channel, galleryService, configurationService, productService); + this.localExtensionManagementService = localExtensionManagementServer.extensionManagementService; } async install(vsix: URI): Promise { @@ -101,14 +103,14 @@ export class RemoteExtensionManagementChannelClient extends ExtensionManagementC const result = new Map(); const extensions = [...(manifest.extensionPack || []), ...(manifest.extensionDependencies || [])]; await this.getDependenciesAndPackedExtensionsRecursively(extensions, result, true, token); - return toArray(result.values()); + return [...result.values()]; } private async getAllWorkspaceDependenciesAndPackedExtensions(manifest: IExtensionManifest, token: CancellationToken): Promise { const result = new Map(); const extensions = [...(manifest.extensionPack || []), ...(manifest.extensionDependencies || [])]; await this.getDependenciesAndPackedExtensionsRecursively(extensions, result, false, token); - return toArray(result.values()); + return [...result.values()]; } private async getDependenciesAndPackedExtensionsRecursively(toGet: string[], result: Map, uiExtension: boolean, token: CancellationToken): Promise { diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index d7ccb3fafec..79ba1b68a98 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -23,6 +23,8 @@ import { assign } from 'vs/base/common/objects'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { productService } from 'vs/workbench/test/browser/workbenchTestServices'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; +import { IUserDataSyncAccountService, UserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; +import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; function createStorageService(instantiationService: TestInstantiationService): IStorageService { let service = instantiationService.get(IStorageService); @@ -51,7 +53,9 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { extensionManagementService, instantiationService.get(IConfigurationService), extensionManagementServerService, - productService + productService, + instantiationService.get(IUserDataAutoSyncService) || instantiationService.stub(IUserDataAutoSyncService, >{ isEnabled() { return false; } }), + instantiationService.get(IUserDataSyncAccountService) || instantiationService.stub(IUserDataSyncAccountService, UserDataSyncAccountService) ); } @@ -371,6 +375,48 @@ suite('ExtensionEnablementService Test', () => { assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a', { localizations: [{ languageId: 'gr', translations: [{ id: 'vscode', path: 'path' }] }] })), false); }); + test('test canChangeEnablement return true for auth extension', () => { + assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a', { authentication: [{ id: 'a', label: 'a' }] })), true); + }); + + test('test canChangeEnablement return true for auth extension when user data sync account does not depends on it', () => { + instantiationService.stub(IUserDataSyncAccountService, >{ + account: { authenticationProviderId: 'b' } + }); + testObject = new TestExtensionEnablementService(instantiationService); + assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a', { authentication: [{ id: 'a', label: 'a' }] })), true); + }); + + test('test canChangeEnablement return true for auth extension when user data sync account depends on it but auto sync is off', () => { + instantiationService.stub(IUserDataSyncAccountService, >{ + account: { authenticationProviderId: 'a' } + }); + testObject = new TestExtensionEnablementService(instantiationService); + assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a', { authentication: [{ id: 'a', label: 'a' }] })), true); + }); + + test('test canChangeEnablement return false for auth extension and user data sync account depends on it and auto sync is on', () => { + instantiationService.stub(IUserDataAutoSyncService, >{ isEnabled() { return true; } }); + instantiationService.stub(IUserDataSyncAccountService, >{ + account: { authenticationProviderId: 'a' } + }); + testObject = new TestExtensionEnablementService(instantiationService); + assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a', { authentication: [{ id: 'a', label: 'a' }] })), false); + }); + + test('test canChangeWorkspaceEnablement return true', () => { + assert.equal(testObject.canChangeWorkspaceEnablement(aLocalExtension('pub.a')), true); + }); + + test('test canChangeWorkspaceEnablement return false if there is no workspace', () => { + instantiationService.stub(IWorkspaceContextService, 'getWorkbenchState', WorkbenchState.EMPTY); + assert.equal(testObject.canChangeWorkspaceEnablement(aLocalExtension('pub.a')), false); + }); + + test('test canChangeWorkspaceEnablement return false for auth extension', () => { + assert.equal(testObject.canChangeWorkspaceEnablement(aLocalExtension('pub.a', { authentication: [{ id: 'a', label: 'a' }] })), false); + }); + test('test canChangeEnablement return false when extensions are disabled in environment', () => { instantiationService.stub(IWorkbenchEnvironmentService, { disableExtensions: true } as IWorkbenchEnvironmentService); testObject = new TestExtensionEnablementService(instantiationService); diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index d0710e77fa2..9e979d28691 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -24,6 +24,8 @@ import { FetchFileSystemProvider } from 'vs/workbench/services/extensions/browse import { Schemas } from 'vs/base/common/network'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { IUserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; export class ExtensionService extends AbstractExtensionService implements IExtensionService { @@ -43,6 +45,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, @IConfigurationService private readonly _configService: IConfigurationService, @IWebExtensionsScannerService private readonly _webExtensionsScannerService: IWebExtensionsScannerService, + @ILifecycleService private readonly _lifecycleService: ILifecycleService, + @IUserDataInitializationService private readonly _userDataInitializationService: IUserDataInitializationService, ) { super( instantiationService, @@ -56,7 +60,12 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._runningLocation = new Map(); - this._initialize(); + // Initialize extensions first and do it only after workbench is ready + this._lifecycleService.when(LifecyclePhase.Ready).then(async () => { + await this._userDataInitializationService.initializeExtensions(this._instantiationService); + this._initialize(); + }); + this._initFetchFileSystem(); } diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 446f74eff84..c89897ab69c 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -15,7 +15,7 @@ import { BetterMergeId } from 'vs/platform/extensionManagement/common/extensionM import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ActivationTimes, ExtensionPointContribution, IExtensionService, IExtensionsStatus, IMessage, IWillActivateEvent, IResponsiveStateChangeEvent, toExtension, IExtensionHost } from 'vs/workbench/services/extensions/common/extensions'; +import { ActivationTimes, ExtensionPointContribution, IExtensionService, IExtensionsStatus, IMessage, IWillActivateEvent, IResponsiveStateChangeEvent, toExtension, IExtensionHost, ActivationKind } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionMessageCollector, ExtensionPoint, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { ResponsiveState } from 'vs/workbench/services/extensions/common/rpcProtocol'; @@ -186,7 +186,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._startExtensionHosts(false, Array.from(this._allRequestedActivateEvents.keys())); } - public activateByEvent(activationEvent: string): Promise { + public activateByEvent(activationEvent: string, activationKind: ActivationKind = ActivationKind.Normal): Promise { if (this._installedExtensionsReady.isOpen()) { // Extensions have been scanned and interpreted @@ -198,20 +198,25 @@ export abstract class AbstractExtensionService extends Disposable implements IEx return NO_OP_VOID_PROMISE; } - return this._activateByEvent(activationEvent); + return this._activateByEvent(activationEvent, activationKind); } else { // Extensions have not been scanned yet. // Record the fact that this activationEvent was requested (in case of a restart) this._allRequestedActivateEvents.add(activationEvent); - return this._installedExtensionsReady.wait().then(() => this._activateByEvent(activationEvent)); + if (activationKind === ActivationKind.Immediate) { + // Do not wait for the normal start-up of the extension host(s) + return this._activateByEvent(activationEvent, activationKind); + } + + return this._installedExtensionsReady.wait().then(() => this._activateByEvent(activationEvent, activationKind)); } } - private _activateByEvent(activationEvent: string): Promise { + private _activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise { const result = Promise.all( - this._extensionHostManagers.map(extHostManager => extHostManager.activateByEvent(activationEvent)) + this._extensionHostManagers.map(extHostManager => extHostManager.activateByEvent(activationEvent, activationKind)) ).then(() => { }); this._onWillActivateByEvent.fire({ event: activationEvent, diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index 484444e968d..d531ae94a20 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -21,7 +21,7 @@ import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { StopWatch } from 'vs/base/common/stopwatch'; import { VSBuffer } from 'vs/base/common/buffer'; -import { IExtensionHost, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionHost, ExtensionHostKind, ActivationKind } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; // Enable to see detailed message communication between window and extension host @@ -48,6 +48,7 @@ export class ExtensionHostManager extends Disposable { */ private _proxy: Promise<{ value: ExtHostExtensionServiceShape; } | null> | null; private _resolveAuthorityAttempt: number; + private _hasStarted = false; constructor( extensionHost: IExtensionHost, @@ -65,6 +66,7 @@ export class ExtensionHostManager extends Disposable { this.onDidExit = this._extensionHost.onExit; this._proxy = this._extensionHost.start()!.then( (protocol) => { + this._hasStarted = true; return { value: this._createExtensionHostCustomers(protocol) }; }, (err) => { @@ -74,7 +76,7 @@ export class ExtensionHostManager extends Disposable { } ); this._proxy.then(() => { - initialActivationEvents.forEach((activationEvent) => this.activateByEvent(activationEvent)); + initialActivationEvents.forEach((activationEvent) => this.activateByEvent(activationEvent, ActivationKind.Normal)); this._register(registerLatencyTestProvider({ measure: () => this.measure() })); @@ -217,14 +219,18 @@ export class ExtensionHostManager extends Disposable { return proxy.$activate(extension, reason); } - public activateByEvent(activationEvent: string): Promise { + public activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise { + if (activationKind === ActivationKind.Immediate && !this._hasStarted) { + return Promise.resolve(); + } + if (!this._cachedActivationEvents.has(activationEvent)) { - this._cachedActivationEvents.set(activationEvent, this._activateByEvent(activationEvent)); + this._cachedActivationEvents.set(activationEvent, this._activateByEvent(activationEvent, activationKind)); } return this._cachedActivationEvents.get(activationEvent)!; } - private async _activateByEvent(activationEvent: string): Promise { + private async _activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise { if (!this._proxy) { return; } @@ -234,7 +240,7 @@ export class ExtensionHostManager extends Disposable { // i.e. the extension host could not be started return; } - return proxy.value.$activateByEvent(activationEvent); + return proxy.value.$activateByEvent(activationEvent, activationKind); } public async getInspectPort(tryEnableInspector: boolean): Promise { diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index 652a2603156..83fb70f44b2 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -140,6 +140,11 @@ export interface IResponsiveStateChangeEvent { isResponsive: boolean; } +export const enum ActivationKind { + Normal = 0, + Immediate = 1 +} + export interface IExtensionService { readonly _serviceBrand: undefined; @@ -177,8 +182,15 @@ export interface IExtensionService { /** * Send an activation event and activate interested extensions. + * + * This will wait for the normal startup of the extension host(s). + * + * In extraordinary circumstances, if the activation event needs to activate + * one or more extensions before the normal startup is finished, then you can use + * `ActivationKind.Immediate`. Please do not use this flag unless really necessary + * and you understand all consequences. */ - activateByEvent(activationEvent: string): Promise; + activateByEvent(activationEvent: string, activationKind?: ActivationKind): Promise; /** * An promise that resolves when the installed extensions are registered after diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index 28b7f1e14b3..c454bf39efa 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -12,7 +12,6 @@ import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/c import { Registry } from 'vs/platform/registry/common/platform'; import { IMessage } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionIdentifier, IExtensionDescription, EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions'; -import { toArray } from 'vs/base/common/arrays'; const schemaRegistry = Registry.as(Extensions.JSONContribution); export type ExtensionKind = 'workspace' | 'ui' | undefined; @@ -446,7 +445,7 @@ export class ExtensionsRegistryImpl { } public getExtensionPoints(): ExtensionPoint[] { - return toArray(this._extensionPoints.values()); + return Array.from(this._extensionPoints.values()); } } diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHostProfiler.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHostProfiler.ts index 914178135b7..342ec2dc402 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHostProfiler.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHostProfiler.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Profile, ProfileNode } from 'v8-inspect-profiler'; +import type { Profile, ProfileNode } from 'v8-inspect-profiler'; import { TernarySearchTree } from 'vs/base/common/map'; import { realpathSync } from 'vs/base/node/extpath'; import { IExtensionHostProfile, IExtensionService, ProfileSegmentId, ProfileSession } from 'vs/workbench/services/extensions/common/extensions'; diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 512b1c906ed..dd041f9cd9a 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -41,6 +41,7 @@ import { WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser import { IExtensionActivationHost as IWorkspaceContainsActivationHost, checkGlobFileExists, checkActivateWorkspaceContainsExtension } from 'vs/workbench/api/common/shared/workspaceContains'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { exists } from 'vs/base/node/pfs'; +import { ILogService } from 'vs/platform/log/common/log'; class DeltaExtensionsQueueItem { constructor( @@ -76,6 +77,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten @IRemoteExplorerService private readonly _remoteExplorerService: IRemoteExplorerService, @IExtensionGalleryService private readonly _extensionGalleryService: IExtensionGalleryService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, + @ILogService private readonly _logService: ILogService, ) { super( instantiationService, @@ -431,6 +433,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten } protected _onExtensionHostCrashed(extensionHost: ExtensionHostManager, code: number, signal: string | null): void { + const activatedExtensions = Array.from(this._extensionHostActiveExtensions.values()); super._onExtensionHostCrashed(extensionHost, code, signal); if (extensionHost.kind === ExtensionHostKind.LocalProcess) { @@ -451,6 +454,9 @@ export class ExtensionService extends AbstractExtensionService implements IExten return; } + const message = `Extension host terminated unexpectedly. The following extensions were running: ${activatedExtensions.map(id => id.value).join(', ')}`; + this._logService.error(message); + this._notificationService.prompt(Severity.Error, nls.localize('extensionService.crash', "Extension host terminated unexpectedly."), [{ label: nls.localize('devTools', "Open Developer Tools"), diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index fdf086f74c7..7ec855ce59b 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -44,6 +44,7 @@ import { joinPath } from 'vs/base/common/resources'; import { Registry } from 'vs/platform/registry/common/platform'; import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; +import { isUUID } from 'vs/base/common/uuid'; export interface ILocalProcessExtensionHostInitData { readonly autoStart: boolean; @@ -182,18 +183,23 @@ export class LocalProcessExtensionHost implements IExtensionHost { opts.execArgv = ['--inspect-port=0']; } - // Enable the crash reporter depending on environment for local reporting - const crashesDirectory = this._environmentService.crashReporterDirectory; - if (crashesDirectory) { - const crashReporterOptions: CrashReporterStartOptions = { + // On linux crash reporter needs to be started on child node processes explicitly + if (platform.isLinux) { + const crashReporterStartOptions: CrashReporterStartOptions = { companyName: this._productService.crashReporter?.companyName || 'Microsoft', productName: this._productService.crashReporter?.productName || this._productService.nameShort, submitURL: '', - uploadToServer: false, - crashesDirectory + uploadToServer: false }; - - opts.env.CRASH_REPORTER_START_OPTIONS = JSON.stringify(crashReporterOptions); + const crashReporterId = this._environmentService.crashReporterId; // crashReporterId is set by the main process only when crash reporting is enabled by the user. + const appcenter = this._productService.appCenter; + const uploadCrashesToServer = !this._environmentService.crashReporterDirectory; // only upload unless --crash-reporter-directory is provided + if (uploadCrashesToServer && appcenter && crashReporterId && isUUID(crashReporterId)) { + const submitURL = appcenter[`linux-x64`]; + crashReporterStartOptions.submitURL = submitURL.concat('&uid=', crashReporterId, '&iid=', crashReporterId, '&sid=', crashReporterId); + crashReporterStartOptions.uploadToServer = true; + } + opts.env.CRASH_REPORTER_START_OPTIONS = JSON.stringify(crashReporterStartOptions); } // Run Extension Host as fork of current process diff --git a/src/vs/workbench/services/extensions/node/proxyResolver.ts b/src/vs/workbench/services/extensions/node/proxyResolver.ts index 9c1fa07fd8e..3a9f49c4471 100644 --- a/src/vs/workbench/services/extensions/node/proxyResolver.ts +++ b/src/vs/workbench/services/extensions/node/proxyResolver.ts @@ -493,9 +493,8 @@ async function readCaCertificates() { } async function readWindowsCaCertificates() { - const winCA = await new Promise((resolve, reject) => { - require(['vscode-windows-ca-certs'], resolve, reject); - }); + // @ts-ignore Windows only + const winCA = await import('vscode-windows-ca-certs'); let ders: any[] = []; const store = winCA(); diff --git a/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts b/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts index 637a617c76c..976d2b1cc19 100644 --- a/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts +++ b/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts @@ -139,11 +139,9 @@ suite('RPCProtocol', () => { let p = bProxy.$m(4, tokenSource.token); p.then((res: number) => { assert.equal(res, 7); - done(null); }, (err) => { assert.fail('should not receive error'); - done(); - }); + }).finally(done); tokenSource.cancel(); }); @@ -153,11 +151,9 @@ suite('RPCProtocol', () => { }; bProxy.$m(4, 1).then((res) => { assert.fail('unexpected'); - done(null); }, (err) => { assert.equal(err.message, 'nope'); - done(null); - }); + }).finally(done); }); test('error promise', function (done) { @@ -166,11 +162,9 @@ suite('RPCProtocol', () => { }; bProxy.$m(4, 1).then((res) => { assert.fail('unexpected'); - done(null); }, (err) => { assert.equal(err, undefined); - done(null); - }); + }).finally(done); }); test('issue #60450: Converting circular structure to JSON', function (done) { @@ -181,11 +175,9 @@ suite('RPCProtocol', () => { }; bProxy.$m(4, 1).then((res) => { assert.equal(res, null); - done(null); }, (err) => { assert.fail('unexpected'); - done(null); - }); + }).finally(done); }); test('issue #72798: null errors are hard to digest', function (done) { @@ -195,11 +187,9 @@ suite('RPCProtocol', () => { }; bProxy.$m(4, 1).then((res) => { assert.fail('unexpected'); - done(null); }, (err) => { assert.equal(err.what, 'what'); - done(null); - }); + }).finally(done); }); test('undefined arguments arrive as null', function () { diff --git a/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts b/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts index 2041723074a..c1e85e6539f 100644 --- a/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts +++ b/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts @@ -36,6 +36,15 @@ self.postMessage = () => console.trace(`'postMessage' has been blocked`); const nativeAddEventLister = addEventListener.bind(self); self.addEventLister = () => console.trace(`'addEventListener' has been blocked`); +(self)['AMDLoader'] = undefined; +(self)['NLSLoaderPlugin'] = undefined; +(self)['define'] = undefined; +(self)['require'] = undefined; +(self)['webkitRequestFileSystem'] = undefined; +(self)['webkitRequestFileSystemSync'] = undefined; +(self)['webkitResolveLocalFileSystemSyncURL'] = undefined; +(self)['webkitResolveLocalFileSystemURL'] = undefined; + if (location.protocol === 'data:') { // make sure new Worker(...) always uses data: const _Worker = Worker; diff --git a/src/vs/workbench/services/host/electron-sandbox/desktopHostService.ts b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts similarity index 96% rename from src/vs/workbench/services/host/electron-sandbox/desktopHostService.ts rename to src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts index ac98ee8f1fa..86aa6287b4a 100644 --- a/src/vs/workbench/services/host/electron-sandbox/desktopHostService.ts +++ b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts @@ -12,7 +12,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows'; import { Disposable } from 'vs/base/common/lifecycle'; -export class DesktopHostService extends Disposable implements IHostService { +export class NativeHostService extends Disposable implements IHostService { declare readonly _serviceBrand: undefined; @@ -95,4 +95,4 @@ export class DesktopHostService extends Disposable implements IHostService { } } -registerSingleton(IHostService, DesktopHostService, true); +registerSingleton(IHostService, NativeHostService, true); diff --git a/src/vs/workbench/services/hover/browser/hover.ts b/src/vs/workbench/services/hover/browser/hover.ts index a504b379d65..f696ebd64e7 100644 --- a/src/vs/workbench/services/hover/browser/hover.ts +++ b/src/vs/workbench/services/hover/browser/hover.ts @@ -109,4 +109,10 @@ export interface IHoverTarget extends IDisposable { * 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; } diff --git a/src/vs/workbench/services/hover/browser/hoverWidget.ts b/src/vs/workbench/services/hover/browser/hoverWidget.ts index 702e4e5143b..622d75418b6 100644 --- a/src/vs/workbench/services/hover/browser/hoverWidget.ts +++ b/src/vs/workbench/services/hover/browser/hoverWidget.ts @@ -16,6 +16,7 @@ import { HoverWidget as BaseHoverWidget, renderHoverAction } from 'vs/base/brows import { Widget } from 'vs/base/browser/ui/widget'; import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; const $ = dom.$; @@ -48,7 +49,8 @@ export class HoverWidget extends Widget { options: IHoverOptions, @IKeybindingService private readonly _keybindingService: IKeybindingService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @IOpenerService private readonly _openerService: IOpenerService + @IOpenerService private readonly _openerService: IOpenerService, + @IWorkbenchLayoutService private readonly _workbenchLayoutService: IWorkbenchLayoutService, ) { super(); @@ -136,11 +138,12 @@ export class HoverWidget extends Widget { this._hover.containerDomNode.classList.remove('right-aligned'); this._hover.contentsDomNode.style.maxHeight = ''; - // Get horizontal alignment and position const targetBounds = this._target.targetElements.map(e => e.getBoundingClientRect()); - const targetLeft = Math.min(...targetBounds.map(e => e.left)); + + // Get horizontal alignment and position + let targetLeft = this._target.x !== undefined ? this._target.x : Math.min(...targetBounds.map(e => e.left)); if (targetLeft + this._hover.containerDomNode.clientWidth >= document.documentElement.clientWidth) { - this._x = document.documentElement.clientWidth; + this._x = document.documentElement.clientWidth - (this._workbenchLayoutService.hasWindowBorder() ? 3 : 1); this._hover.containerDomNode.classList.add('right-aligned'); } else { this._x = targetLeft; diff --git a/src/vs/workbench/services/hover/browser/media/hover.css b/src/vs/workbench/services/hover/browser/media/hover.css index 47d8ab484c6..6514844fd10 100644 --- a/src/vs/workbench/services/hover/browser/media/hover.css +++ b/src/vs/workbench/services/hover/browser/media/hover.css @@ -18,6 +18,11 @@ color: #3794ff; } +.monaco-workbench .workbench-hover.right-aligned { + /* The context view service wraps strangely when it's right up against the edge without this */ + left: 1px; +} + .monaco-workbench .workbench-hover.right-aligned .hover-row.status-bar .actions { flex-direction: row-reverse; } diff --git a/src/vs/workbench/services/keybinding/browser/keybindingService.ts b/src/vs/workbench/services/keybinding/browser/keybindingService.ts index a9dc027018d..34ea342cc80 100644 --- a/src/vs/workbench/services/keybinding/browser/keybindingService.ts +++ b/src/vs/workbench/services/keybinding/browser/keybindingService.ts @@ -194,7 +194,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { @ILogService logService: ILogService, @IKeymapService private readonly keymapService: IKeymapService ) { - super(contextKeyService, commandService, telemetryService, notificationService); + super(contextKeyService, commandService, telemetryService, notificationService, logService); this.updateSchema(); diff --git a/src/vs/workbench/services/keybinding/electron-browser/keybinding.contribution.ts b/src/vs/workbench/services/keybinding/electron-browser/keybinding.contribution.ts deleted file mode 100644 index 230ed0ed670..00000000000 --- a/src/vs/workbench/services/keybinding/electron-browser/keybinding.contribution.ts +++ /dev/null @@ -1,37 +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 nls from 'vs/nls'; -import { release } from 'os'; -import { OS, OperatingSystem } from 'vs/base/common/platform'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions as ConfigExtensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; - -const configurationRegistry = Registry.as(ConfigExtensions.Configuration); -const keyboardConfiguration: IConfigurationNode = { - 'id': 'keyboard', - 'order': 15, - 'type': 'object', - 'title': nls.localize('keyboardConfigurationTitle', "Keyboard"), - 'properties': { - 'keyboard.touchbar.enabled': { - 'type': 'boolean', - 'default': true, - 'description': nls.localize('touchbar.enabled', "Enables the macOS touchbar buttons on the keyboard if available."), - 'included': OS === OperatingSystem.Macintosh && parseFloat(release()) >= 16 // Minimum: macOS Sierra (10.12.x = darwin 16.x) - }, - 'keyboard.touchbar.ignored': { - 'type': 'array', - 'items': { - 'type': 'string' - }, - 'default': [], - 'markdownDescription': nls.localize('touchbar.ignored', 'A set of identifiers for entries in the touchbar that should not show up (for example `workbench.action.navigateBack`.'), - 'included': OS === OperatingSystem.Macintosh && parseFloat(release()) >= 16 // Minimum: macOS Sierra (10.12.x = darwin 16.x) - } - } -}; - -configurationRegistry.registerConfiguration(keyboardConfiguration); diff --git a/src/vs/workbench/services/keybinding/electron-browser/nativeKeymapService.ts b/src/vs/workbench/services/keybinding/electron-browser/nativeKeymapService.ts index 6ef2edda43d..ab410a95dc5 100644 --- a/src/vs/workbench/services/keybinding/electron-browser/nativeKeymapService.ts +++ b/src/vs/workbench/services/keybinding/electron-browser/nativeKeymapService.ts @@ -15,6 +15,7 @@ import { OS, OperatingSystem } from 'vs/base/common/platform'; import { WindowsKeyboardMapper, windowsKeyboardMappingEquals } from 'vs/workbench/services/keybinding/common/windowsKeyboardMapper'; import { MacLinuxKeyboardMapper, macLinuxKeyboardMappingEquals, IMacLinuxKeyboardMapping } from 'vs/workbench/services/keybinding/common/macLinuxKeyboardMapper'; import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; +import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; export class KeyboardMapperFactory { public static readonly INSTANCE = new KeyboardMapperFactory(); @@ -134,7 +135,7 @@ export class KeyboardMapperFactory { class NativeKeymapService extends Disposable implements IKeymapService { public _serviceBrand: undefined; - private readonly _onDidChangeKeyboardMapper = new Emitter(); + private readonly _onDidChangeKeyboardMapper = this._register(new Emitter()); public readonly onDidChangeKeyboardMapper: Event = this._onDidChangeKeyboardMapper.event; constructor() { @@ -143,6 +144,10 @@ class NativeKeymapService extends Disposable implements IKeymapService { this._register(KeyboardMapperFactory.INSTANCE.onDidChangeKeyboardMapper(() => { this._onDidChangeKeyboardMapper.fire(); })); + + ipcRenderer.on('vscode:keyboardLayoutChanged', () => { + KeyboardMapperFactory.INSTANCE._onKeyboardLayoutChanged(); + }); } getKeyboardMapper(dispatchConfig: DispatchConfig): IKeyboardMapper { diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index b1213ff2ce7..d8ff60045c5 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -33,9 +33,8 @@ export function positionToString(position: Position): string { case Position.LEFT: return 'left'; case Position.RIGHT: return 'right'; case Position.BOTTOM: return 'bottom'; + default: return 'bottom'; } - - return 'bottom'; } const positionsByString: { [key: string]: Position } = { diff --git a/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts b/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts index 7071d3d1de0..883af93ee8e 100644 --- a/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts @@ -135,16 +135,16 @@ export class NativeLifecycleService extends AbstractLifecycleService { let message: string; switch (reason) { case ShutdownReason.CLOSE: - message = localize('errorClose', "An unexpected error prevented the window from closing ({0}).", toErrorMessage(error)); + message = localize('errorClose', "An unexpected error was thrown while attempting to close the window ({0}).", toErrorMessage(error)); break; case ShutdownReason.QUIT: - message = localize('errorQuit', "An unexpected error prevented the application from closing ({0}).", toErrorMessage(error)); + message = localize('errorQuit', "An unexpected error was thrown while attempting to quit the application ({0}).", toErrorMessage(error)); break; case ShutdownReason.RELOAD: - message = localize('errorReload', "An unexpected error prevented the window from reloading ({0}).", toErrorMessage(error)); + message = localize('errorReload', "An unexpected error was thrown while attempting to reload the window ({0}).", toErrorMessage(error)); break; case ShutdownReason.LOAD: - message = localize('errorLoad', "An unexpected error prevented the window from changing it's workspace ({0}).", toErrorMessage(error)); + message = localize('errorLoad', "An unexpected error was thrown while attempting to change the workspace of the window ({0}).", toErrorMessage(error)); break; } diff --git a/src/vs/workbench/services/log/electron-browser/logService.ts b/src/vs/workbench/services/log/electron-browser/logService.ts index caa1bd713c0..6457c582eb4 100644 --- a/src/vs/workbench/services/log/electron-browser/logService.ts +++ b/src/vs/workbench/services/log/electron-browser/logService.ts @@ -14,7 +14,7 @@ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } f import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -export class DesktopLogService extends DelegatedLogService { +export class NativeLogService extends DelegatedLogService { private readonly bufferSpdLogService: BufferLogService | undefined; private readonly windowId: number; @@ -62,11 +62,11 @@ export class DesktopLogService extends DelegatedLogService { } } -class DesktopLogServiceInitContribution implements IWorkbenchContribution { +class NativeLogServiceInitContribution implements IWorkbenchContribution { constructor(@ILogService logService: ILogService) { - if (logService instanceof DesktopLogService) { + if (logService instanceof NativeLogService) { logService.init(); } } } -Registry.as(Extensions.Workbench).registerWorkbenchContribution(DesktopLogServiceInitContribution, LifecyclePhase.Restored); +Registry.as(Extensions.Workbench).registerWorkbenchContribution(NativeLogServiceInitContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/services/path/common/pathService.ts b/src/vs/workbench/services/path/common/pathService.ts index c2e1b21bd5a..8d877ba3746 100644 --- a/src/vs/workbench/services/path/common/pathService.ts +++ b/src/vs/workbench/services/path/common/pathService.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -export const IPathService = createDecorator('path'); +export const IPathService = createDecorator('pathService'); /** * Provides access to path related properties that will match the diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index 0d414997afc..0033c6bd263 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -40,7 +40,7 @@ export interface ISettingsGroup { title: string; titleRange: IRange; sections: ISettingsSection[]; - contributedByExtension: boolean; + extensionInfo?: IConfigurationExtensionInfo; } export interface ISettingsSection { diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index 1172b5b0b32..16ff41f32e4 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { flatten, tail, find, coalesce } from 'vs/base/common/arrays'; +import { flatten, tail, coalesce } from 'vs/base/common/arrays'; import { IStringDictionary } from 'vs/base/common/collections'; import { Emitter, Event } from 'vs/base/common/event'; import { JSONVisitor, visit } from 'vs/base/common/json'; import { Disposable, IReference } from 'vs/base/common/lifecycle'; -import { assign } from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; @@ -198,7 +197,7 @@ export class SettingsEditorModel extends AbstractSettingsModel implements ISetti }], title: modelGroup.title, titleRange: modelGroup.titleRange, - contributedByExtension: !!modelGroup.contributedByExtension + extensionInfo: modelGroup.extensionInfo }; } @@ -291,7 +290,7 @@ function parse(model: ITextModel, isSettingsProperty: (currentProperty: string, endLineNumber: valueEndPosition.lineNumber, endColumn: valueEndPosition.column }; - setting.range = assign(setting.range, { + setting.range = Object.assign(setting.range, { endLineNumber: valueEndPosition.lineNumber, endColumn: valueEndPosition.column }); @@ -357,11 +356,11 @@ function parse(model: ITextModel, isSettingsProperty: (currentProperty: string, const setting = previousParents.length === settingsPropertyIndex + 1 ? settings[settings.length - 1] : overrideSetting!.overrides![overrideSetting!.overrides!.length - 1]; if (setting) { const valueEndPosition = model.getPositionAt(offset + length); - setting.valueRange = assign(setting.valueRange, { + setting.valueRange = Object.assign(setting.valueRange, { endLineNumber: valueEndPosition.lineNumber, endColumn: valueEndPosition.column }); - setting.range = assign(setting.range, { + setting.range = Object.assign(setting.range, { endLineNumber: valueEndPosition.lineNumber, endColumn: valueEndPosition.column }); @@ -393,11 +392,11 @@ function parse(model: ITextModel, isSettingsProperty: (currentProperty: string, const setting = previousParents.length === settingsPropertyIndex + 1 ? settings[settings.length - 1] : overrideSetting!.overrides![overrideSetting!.overrides!.length - 1]; if (setting) { const valueEndPosition = model.getPositionAt(offset + length); - setting.valueRange = assign(setting.valueRange, { + setting.valueRange = Object.assign(setting.valueRange, { endLineNumber: valueEndPosition.lineNumber, endColumn: valueEndPosition.column }); - setting.range = assign(setting.range, { + setting.range = Object.assign(setting.range, { endLineNumber: valueEndPosition.lineNumber, endColumn: valueEndPosition.column }); @@ -558,16 +557,16 @@ export class DefaultSettings extends Disposable { seenSettings = seenSettings ? seenSettings : {}; let title = config.title; if (!title) { - const configWithTitleAndSameId = find(configurations, c => (c.id === config.id) && c.title); + const configWithTitleAndSameId = configurations.find(c => (c.id === config.id) && c.title); if (configWithTitleAndSameId) { title = configWithTitleAndSameId.title; } } if (title) { if (!settingsGroup) { - settingsGroup = find(result, g => g.title === title); + settingsGroup = result.find(g => g.title === title && g.extensionInfo?.id === config.extensionInfo?.id); if (!settingsGroup) { - settingsGroup = { sections: [{ settings: [] }], id: config.id || '', title: title || '', titleRange: nullRange, range: nullRange, contributedByExtension: !!config.extensionInfo }; + settingsGroup = { sections: [{ settings: [] }], id: config.id || '', title: title || '', titleRange: nullRange, range: nullRange, extensionInfo: config.extensionInfo }; result.push(settingsGroup); } } else { @@ -576,7 +575,7 @@ export class DefaultSettings extends Disposable { } if (config.properties) { if (!settingsGroup) { - settingsGroup = { sections: [{ settings: [] }], id: config.id || '', title: config.id || '', titleRange: nullRange, range: nullRange, contributedByExtension: !!config.extensionInfo }; + settingsGroup = { sections: [{ settings: [] }], id: config.id || '', title: config.id || '', titleRange: nullRange, range: nullRange, extensionInfo: config.extensionInfo }; result.push(settingsGroup); } const configurationSettings: ISetting[] = []; diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index a8232c97d59..99a8e1489c2 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -48,15 +48,8 @@ export interface Tunnel { closeable?: boolean; } -function ToLocalHost(host: string): string { - if (host === '127.0.0.1') { - host = 'localhost'; - } - return host; -} - export function MakeAddress(host: string, port: number): string { - return ToLocalHost(host) + ':' + port; + return host + ':' + port; } export class TunnelModel extends Disposable { @@ -218,7 +211,7 @@ export class TunnelModel extends Disposable { const nullIndex = value.detail.indexOf('\0'); const detail = value.detail.substr(0, nullIndex > 0 ? nullIndex : value.detail.length).trim(); return { - host: ToLocalHost(value.host), + host: value.host, port: value.port, detail }; diff --git a/src/vs/workbench/services/request/browser/requestService.ts b/src/vs/workbench/services/request/browser/requestService.ts index b9315a52ea2..16aa984a3f3 100644 --- a/src/vs/workbench/services/request/browser/requestService.ts +++ b/src/vs/workbench/services/request/browser/requestService.ts @@ -10,8 +10,6 @@ import { ILogService } from 'vs/platform/log/common/log'; import { RequestChannelClient } from 'vs/platform/request/common/requestIpc'; import { IRemoteAgentService, IRemoteAgentConnection } from 'vs/workbench/services/remote/common/remoteAgentService'; import { RequestService } from 'vs/platform/request/browser/requestService'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IRequestService } from 'vs/platform/request/common/request'; export class BrowserRequestService extends RequestService { @@ -44,5 +42,3 @@ export class BrowserRequestService extends RequestService { return connection.withChannel('request', channel => RequestChannelClient.request(channel, options, token)); } } - -registerSingleton(IRequestService, BrowserRequestService, true); diff --git a/src/vs/workbench/services/search/common/replace.ts b/src/vs/workbench/services/search/common/replace.ts index 9dd7567231c..66b67d620bb 100644 --- a/src/vs/workbench/services/search/common/replace.ts +++ b/src/vs/workbench/services/search/common/replace.ts @@ -13,6 +13,7 @@ export class ReplacePattern { private _replacePattern: string; private _hasParameters: boolean = false; private _regExp: RegExp; + private _caseOpsRegExp: RegExp; constructor(replaceString: string, searchPatternInfo: IPatternInfo) constructor(replaceString: string, parseParameters: boolean, regEx: RegExp) @@ -37,6 +38,8 @@ export class ReplacePattern { if (this._regExp.global) { this._regExp = strings.createRegExp(this._regExp.source, true, { matchCase: !this._regExp.ignoreCase, wholeWord: false, multiline: this._regExp.multiline, global: false }); } + + this._caseOpsRegExp = new RegExp(/([^\\]*?)((?:\\[uUlL])+?|)(\$[0-9]+)(.*?)/g); } get hasParameters(): boolean { @@ -60,10 +63,10 @@ export class ReplacePattern { const match = this._regExp.exec(text); if (match) { if (this.hasParameters) { + const replaceString = this.replaceWithCaseOperations(text, this._regExp, this.buildReplaceString(match, preserveCase)); if (match[0] === text) { - return text.replace(this._regExp, this.buildReplaceString(match, preserveCase)); + return replaceString; } - const replaceString = text.replace(this._regExp, this.buildReplaceString(match, preserveCase)); return replaceString.substr(match.index, match[0].length - (text.length - replaceString.length)); } return this.buildReplaceString(match, preserveCase); @@ -72,6 +75,84 @@ export class ReplacePattern { return null; } + /** + * replaceWithCaseOperations applies case operations to relevant replacement strings and applies + * the affected $N arguments. It then passes unaffected $N arguments through to string.replace(). + * + * \u => upper-cases one character in a match. + * \U => upper-cases ALL remaining characters in a match. + * \l => lower-cases one character in a match. + * \L => lower-cases ALL remaining characters in a match. + */ + private replaceWithCaseOperations(text: string, regex: RegExp, replaceString: string): string { + // Short-circuit the common path. + if (!/\\[uUlL]/.test(replaceString)) { + return text.replace(regex, replaceString); + } + // Store the values of the search parameters. + const firstMatch = regex.exec(text); + if (firstMatch === null) { + return text.replace(regex, replaceString); + } + + let patMatch: RegExpExecArray | null; + let newReplaceString = ''; + let lastIndex = 0; + let lastMatch = ''; + // For each annotated $N, perform text processing on the parameters and perform the substitution. + while ((patMatch = this._caseOpsRegExp.exec(replaceString)) !== null) { + lastIndex = patMatch.index; + const fullMatch = patMatch[0]; + lastMatch = fullMatch; + let caseOps = patMatch[2]; // \u, \l\u, etc. + const money = patMatch[3]; // $1, $2, etc. + + if (!caseOps) { + newReplaceString += fullMatch; + continue; + } + const replacement = firstMatch[parseInt(money.slice(1))]; + if (!replacement) { + newReplaceString += fullMatch; + continue; + } + const replacementLen = replacement.length; + + newReplaceString += patMatch[1]; // prefix + caseOps = caseOps.replace(/\\/g, ''); + let i = 0; + for (; i < caseOps.length; i++) { + switch (caseOps[i]) { + case 'U': + newReplaceString += replacement.slice(i).toUpperCase(); + i = replacementLen; + break; + case 'u': + newReplaceString += replacement[i].toUpperCase(); + break; + case 'L': + newReplaceString += replacement.slice(i).toLowerCase(); + i = replacementLen; + break; + case 'l': + newReplaceString += replacement[i].toLowerCase(); + break; + } + } + // Append any remaining replacement string content not covered by case operations. + if (i < replacementLen) { + newReplaceString += replacement.slice(i); + } + + newReplaceString += patMatch[4]; // suffix + } + + // Append any remaining trailing content after the final regex match. + newReplaceString += replaceString.slice(lastIndex + lastMatch.length); + + return text.replace(regex, newReplaceString); + } + public buildReplaceString(matches: string[] | null, preserveCase?: boolean): string { if (preserveCase) { return buildReplaceStringWithCasePreserved(matches, this._replacePattern); diff --git a/src/vs/workbench/services/search/common/textSearchManager.ts b/src/vs/workbench/services/search/common/textSearchManager.ts index 223374a7bdd..4913240ba05 100644 --- a/src/vs/workbench/services/search/common/textSearchManager.ts +++ b/src/vs/workbench/services/search/common/textSearchManager.ts @@ -54,7 +54,7 @@ export class TextSearchManager { const newResultSize = this.resultSize(result); this.resultCount += newResultSize; - if (newResultSize > 0) { + if (newResultSize > 0 || !extensionResultIsMatch(result)) { this.collector!.add(result, folderIdx); } } @@ -83,10 +83,15 @@ export class TextSearchManager { } private resultSize(result: TextSearchResult): number { - const match = result; - return Array.isArray(match.ranges) ? - match.ranges.length : - 1; + if (extensionResultIsMatch(result)) { + return Array.isArray(result.ranges) ? + result.ranges.length : + 1; + } + else { + // #104400 context lines shoudn't count towards result count + return 0; + } } private trimResultToSize(result: TextSearchMatch, size: number): TextSearchMatch { diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts index 006b8fec0df..a22fd8ba3b9 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts @@ -504,7 +504,7 @@ export function spreadGlobComponents(globArg: string): string[] { export function unicodeEscapesToPCRE2(pattern: string): string { // Match \u1234 - const unicodePattern = /((?:[^\\]|^)(?:\\\\)*)\\u([a-z0-9]{4})/g; + const unicodePattern = /((?:[^\\]|^)(?:\\\\)*)\\u([a-z0-9]{4})/gi; while (pattern.match(unicodePattern)) { pattern = pattern.replace(unicodePattern, `$1\\x{$2}`); @@ -512,7 +512,7 @@ export function unicodeEscapesToPCRE2(pattern: string): string { // Match \u{1234} // \u with 5-6 characters will be left alone because \x only takes 4 characters. - const unicodePatternWithBraces = /((?:[^\\]|^)(?:\\\\)*)\\u\{([a-z0-9]{4})\}/g; + const unicodePatternWithBraces = /((?:[^\\]|^)(?:\\\\)*)\\u\{([a-z0-9]{4})\}/gi; while (pattern.match(unicodePatternWithBraces)) { pattern = pattern.replace(unicodePatternWithBraces, `$1\\x{$2}`); } diff --git a/src/vs/workbench/services/search/test/common/replace.test.ts b/src/vs/workbench/services/search/test/common/replace.test.ts index 7180e0fb43e..3c18ccb9e14 100644 --- a/src/vs/workbench/services/search/test/common/replace.test.ts +++ b/src/vs/workbench/services/search/test/common/replace.test.ts @@ -140,6 +140,12 @@ suite('Replace Pattern test', () => { assert.equal('cat ()', actual); }); + test('case operations', () => { + let testObject = new ReplacePattern('a\\u$1l\\u\\l\\U$2M$3n', { pattern: 'a(l)l(good)m(e)n', isRegExp: true }); + let actual = testObject.getReplaceString('allgoodmen'); + assert.equal('aLlGoODMen', actual); + }); + test('get replace string for no matches', () => { let testObject = new ReplacePattern('hello', { pattern: 'bla', isRegExp: true }); let actual = testObject.getReplaceString('foo'); diff --git a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts index 05fc07f6214..6c7d8296a2d 100644 --- a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts +++ b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts @@ -20,6 +20,7 @@ suite('RipgrepTextSearchEngine', () => { assert.equal(unicodeEscapesToPCRE2('\\u{1234}'), '\\x{1234}'); assert.equal(unicodeEscapesToPCRE2('\\u{1234}\\u{0001}'), '\\x{1234}\\x{0001}'); assert.equal(unicodeEscapesToPCRE2('foo\\u{1234}bar'), 'foo\\x{1234}bar'); + assert.equal(unicodeEscapesToPCRE2('[\\u00A0-\\u00FF]'), '[\\x{00A0}-\\x{00FF}]'); assert.equal(unicodeEscapesToPCRE2('foo\\u{123456}7bar'), 'foo\\u{123456}7bar'); assert.equal(unicodeEscapesToPCRE2('\\u123'), '\\u123'); diff --git a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts index d4a98b3fade..6567c19b739 100644 --- a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts +++ b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts @@ -24,7 +24,7 @@ import { ExtensionMessageCollector } from 'vs/workbench/services/extensions/comm import { ITMSyntaxExtensionPoint, grammarsExtPoint } from 'vs/workbench/services/textMate/common/TMGrammars'; import { ITextMateService } from 'vs/workbench/services/textMate/common/textMateService'; import { ITextMateThemingRule, IWorkbenchThemeService, IWorkbenchColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { IGrammar, StackElement, IOnigLib, IRawTheme } from 'vscode-textmate'; +import type { IGrammar, StackElement, IOnigLib, IRawTheme } from 'vscode-textmate'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IValidGrammarDefinition, IValidEmbeddedLanguagesMap, IValidTokenTypeMap } from 'vs/workbench/services/textMate/common/TMScopeRegistry'; diff --git a/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts b/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts index 187d10b3ba8..955eed45850 100644 --- a/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts +++ b/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { LanguageId } from 'vs/editor/common/modes'; -import { IGrammar, Registry, StackElement, IRawTheme, IOnigLib } from 'vscode-textmate'; +import type { IGrammar, Registry, StackElement, IRawTheme, IOnigLib } from 'vscode-textmate'; import { Disposable } from 'vs/base/common/lifecycle'; import { TMScopeRegistry, IValidGrammarDefinition, IValidEmbeddedLanguagesMap } from 'vs/workbench/services/textMate/common/TMScopeRegistry'; diff --git a/src/vs/workbench/services/textMate/electron-browser/textMateService.ts b/src/vs/workbench/services/textMate/electron-browser/textMateService.ts index 87341b9950f..1b01a674aa1 100644 --- a/src/vs/workbench/services/textMate/electron-browser/textMateService.ts +++ b/src/vs/workbench/services/textMate/electron-browser/textMateService.ts @@ -13,7 +13,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { createWebWorker, MonacoWebWorker } from 'vs/editor/common/services/webWorker'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { IRawTheme } from 'vscode-textmate'; +import type { IRawTheme } from 'vscode-textmate'; import { IValidGrammarDefinition } from 'vs/workbench/services/textMate/common/TMScopeRegistry'; import { TextMateWorker } from 'vs/workbench/services/textMate/electron-browser/textMateWorker'; import { ITextModel } from 'vs/editor/common/model'; diff --git a/src/vs/workbench/services/textMate/electron-browser/textMateWorker.ts b/src/vs/workbench/services/textMate/electron-browser/textMateWorker.ts index de904376230..8a3f0ffb48a 100644 --- a/src/vs/workbench/services/textMate/electron-browser/textMateWorker.ts +++ b/src/vs/workbench/services/textMate/electron-browser/textMateWorker.ts @@ -11,7 +11,7 @@ import { TMGrammarFactory, ICreateGrammarResult } from 'vs/workbench/services/te import { IModelChangedEvent, MirrorTextModel } from 'vs/editor/common/model/mirrorTextModel'; import { TextMateWorkerHost } from 'vs/workbench/services/textMate/electron-browser/textMateService'; import { TokenizationStateStore } from 'vs/editor/common/model/textModelTokens'; -import { IGrammar, StackElement, IRawTheme, IOnigLib } from 'vscode-textmate'; +import type { IGrammar, StackElement, IRawTheme, IOnigLib } from 'vscode-textmate'; import { MultilineTokensBuilder, countEOL } from 'vs/editor/common/model/tokensStore'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 9fe141b27b7..bf0805bb0c1 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -462,13 +462,17 @@ export abstract class AbstractTextFileService extends Disposable implements ITex } suggestFilename(mode: string, untitledName: string) { - const extension = this.modeService.getExtensions(mode)[0]; + const languageName = this.modeService.getLanguageName(mode); + if (!languageName) { + return untitledName; + } + const extension = this.modeService.getExtensions(languageName)[0]; if (extension) { if (!untitledName.endsWith(extension)) { return untitledName + extension; } } - const filename = this.modeService.getFilenames(mode)[0]; + const filename = this.modeService.getFilenames(languageName)[0]; return filename || untitledName; } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 1bb7ac6975d..721d459f8d4 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -19,7 +19,7 @@ import { ITextBufferFactory, ITextModel } from 'vs/editor/common/model'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ILogService } from 'vs/platform/log/common/log'; import { basename } from 'vs/base/common/path'; -import { IWorkingCopyService, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { ILabelService } from 'vs/platform/label/common/label'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -65,7 +65,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil //#endregion - readonly capabilities = 0; + readonly capabilities = WorkingCopyCapabilities.None; readonly name = basename(this.labelService.getUriLabel(this.resource)); diff --git a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts index 05dfa205077..19add1a54b2 100644 --- a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts +++ b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts @@ -126,7 +126,7 @@ export class NativeTextFileService extends AbstractTextFileService { } try { - return super.write(resource, value, options); + return await super.write(resource, value, options); } catch (error) { // In case of permission denied, we need to check for readonly diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index e537e332668..fb6173c005e 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -33,6 +33,7 @@ import { updateColorThemeConfigurationSchemas, updateFileIconThemeConfigurationS import { ProductIconThemeData, DEFAULT_PRODUCT_ICON_THEME_ID } from 'vs/workbench/services/themes/browser/productIconThemeData'; import { registerProductIconThemeSchemas } from 'vs/workbench/services/themes/common/productIconThemeSchema'; import { ILogService } from 'vs/platform/log/common/log'; +import { isWeb } from 'vs/base/common/platform'; // implementation @@ -90,20 +91,24 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { private readonly onProductIconThemeChange: Emitter; private readonly productIconThemeWatcher: ThemeFileWatcher; + private isOSInHighContrast: boolean; // tracking the high contrast state of the OS eventauilly should go out to a seperate service + private themeSettingIdBeforeSchemeSwitch: string | undefined; + constructor( @IExtensionService extensionService: IExtensionService, @IStorageService private readonly storageService: IStorageService, @IConfigurationService private readonly configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService readonly environmentService: IWorkbenchEnvironmentService, @IFileService private readonly fileService: IFileService, @IExtensionResourceLoaderService private readonly extensionResourceLoaderService: IExtensionResourceLoaderService, @IWorkbenchLayoutService readonly layoutService: IWorkbenchLayoutService, @ILogService private readonly logService: ILogService ) { this.container = layoutService.container; - const defaultThemeType = environmentService.configuration.defaultThemeType || DARK; - this.settings = new ThemeConfiguration(configurationService, defaultThemeType); + this.settings = new ThemeConfiguration(configurationService); + + this.isOSInHighContrast = !!environmentService.configuration.highContrast; this.colorThemeRegistry = new ThemeRegistry(extensionService, colorThemesExtPoint, ColorThemeData.fromExtensionTheme); this.colorThemeWatcher = new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentColorTheme.bind(this)); @@ -124,11 +129,20 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { // themes are loaded asynchronously, we need to initialize // a color theme document with good defaults until the theme is loaded let themeData: ColorThemeData | undefined = ColorThemeData.fromStorageData(this.storageService); - if (environmentService.configuration.highContrast && themeData?.baseTheme !== HIGH_CONTRAST) { - themeData = ColorThemeData.createUnloadedThemeForThemeType(HIGH_CONTRAST); + + // the preferred color scheme (high contrast, light, dark) has changed since the last start + const preferredColorScheme = this.getPreferredColorScheme(); + if (preferredColorScheme && themeData?.baseTheme !== preferredColorScheme && this.storageService.get(PERSISTED_OS_COLOR_SCHEME, StorageScope.GLOBAL) !== preferredColorScheme) { + themeData = ColorThemeData.createUnloadedThemeForThemeType(preferredColorScheme); } if (!themeData) { - themeData = ColorThemeData.createUnloadedThemeForThemeType(defaultThemeType); + const initialColorTheme = environmentService.options?.initialColorTheme; + if (initialColorTheme) { + themeData = ColorThemeData.createUnloadedThemeForThemeType(initialColorTheme.themeType, initialColorTheme.colors); + } + } + if (!themeData) { + themeData = ColorThemeData.createUnloadedThemeForThemeType(isWeb ? LIGHT : DARK); } themeData.setCustomizations(this.settings); this.applyTheme(themeData, undefined, true); @@ -161,9 +175,8 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } const theme = await this.colorThemeRegistry.findThemeBySettingsId(this.settings.colorTheme, DEFAULT_COLOR_THEME_ID); - const persistedColorScheme = this.storageService.get(PERSISTED_OS_COLOR_SCHEME, StorageScope.GLOBAL); const preferredColorScheme = this.getPreferredColorScheme(); - if (persistedColorScheme && preferredColorScheme && persistedColorScheme !== preferredColorScheme) { + if (preferredColorScheme && theme?.type !== preferredColorScheme) { return this.applyPreferredColorTheme(preferredColorScheme); } return this.setColorTheme(theme && theme.id, undefined); @@ -195,7 +208,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { if (e.affectsConfiguration(ThemeSettings.COLOR_THEME)) { this.restoreColorTheme(); } - if (e.affectsConfiguration(ThemeSettings.DETECT_COLOR_SCHEME)) { + if (e.affectsConfiguration(ThemeSettings.DETECT_COLOR_SCHEME) || e.affectsConfiguration(ThemeSettings.DETECT_HC)) { this.handlePreferredSchemeUpdated(); } if (e.affectsConfiguration(ThemeSettings.PREFERRED_DARK_THEME) && this.getPreferredColorScheme() === DARK) { @@ -309,18 +322,37 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { window.matchMedia('(prefers-color-scheme: dark)').addListener(async () => this.handlePreferredSchemeUpdated()); } + public setOSHighContrast(highContrast: boolean): void { + if (this.isOSInHighContrast !== highContrast) { + this.isOSInHighContrast = highContrast; + this.handlePreferredSchemeUpdated(); + } + } + private async handlePreferredSchemeUpdated() { const scheme = this.getPreferredColorScheme(); + + const prevScheme = this.storageService.get(PERSISTED_OS_COLOR_SCHEME, StorageScope.GLOBAL); this.storageService.store(PERSISTED_OS_COLOR_SCHEME, scheme, StorageScope.GLOBAL); if (scheme) { + if (!prevScheme) { + // remember the theme before scheme switching + this.themeSettingIdBeforeSchemeSwitch = this.settings.colorTheme; + } return this.applyPreferredColorTheme(scheme); + } else if (prevScheme && this.themeSettingIdBeforeSchemeSwitch) { + // reapply the theme before scheme switching + const theme = await this.colorThemeRegistry.findThemeBySettingsId(this.themeSettingIdBeforeSchemeSwitch, undefined); + if (theme) { + this.setColorTheme(theme.id, 'auto'); + } } return undefined; } private getPreferredColorScheme(): ThemeType | undefined { const detectHCThemeSetting = this.configurationService.getValue(ThemeSettings.DETECT_HC); - if (this.environmentService.configuration.highContrast && detectHCThemeSetting) { + if (this.isOSInHighContrast && detectHCThemeSetting) { return HIGH_CONTRAST; } if (this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME)) { @@ -398,7 +430,6 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { return false; } - private updateDynamicCSSRules(themeData: IColorTheme) { const cssRules = new Set(); const ruleCollector = { diff --git a/src/vs/workbench/services/themes/common/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts index 50bbe176e9e..b8c41191476 100644 --- a/src/vs/workbench/services/themes/common/colorThemeData.ts +++ b/src/vs/workbench/services/themes/common/colorThemeData.ts @@ -550,15 +550,20 @@ export class ColorThemeData implements IWorkbenchColorTheme { // constructors - static createUnloadedThemeForThemeType(themeType: ThemeType): ColorThemeData { - return ColorThemeData.createUnloadedTheme(getThemeTypeSelector(themeType)); + static createUnloadedThemeForThemeType(themeType: ThemeType, colorMap?: { [id: string]: string }): ColorThemeData { + return ColorThemeData.createUnloadedTheme(getThemeTypeSelector(themeType), colorMap); } - static createUnloadedTheme(id: string): ColorThemeData { + static createUnloadedTheme(id: string, colorMap?: { [id: string]: string }): ColorThemeData { let themeData = new ColorThemeData(id, '', '__' + id); themeData.isLoaded = false; themeData.themeTokenColors = []; themeData.watch = false; + if (colorMap) { + for (let id in colorMap) { + themeData.colorMap[id] = Color.fromHex(colorMap[id]); + } + } return themeData; } diff --git a/src/vs/workbench/services/themes/common/themeConfiguration.ts b/src/vs/workbench/services/themes/common/themeConfiguration.ts index 4bacbe49d3d..cd4ad6203f5 100644 --- a/src/vs/workbench/services/themes/common/themeConfiguration.ts +++ b/src/vs/workbench/services/themes/common/themeConfiguration.ts @@ -14,7 +14,7 @@ import { workbenchColorsSchemaId } from 'vs/platform/theme/common/colorRegistry' import { tokenStylingSchemaId } from 'vs/platform/theme/common/tokenClassificationRegistry'; import { ThemeSettings, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IColorCustomizations, ITokenColorCustomizations, IWorkbenchProductIconTheme, ISemanticTokenColorCustomizations, IExperimentalSemanticTokenColorCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { ThemeType, HIGH_CONTRAST, LIGHT } from 'vs/platform/theme/common/themeService'; +import { isMacintosh, isWeb, isWindows } from 'vs/base/common/platform'; const DEFAULT_THEME_DARK_SETTING_VALUE = 'Default Dark+'; const DEFAULT_THEME_LIGHT_SETTING_VALUE = 'Default Light+'; @@ -33,7 +33,7 @@ const colorThemeSettingEnumDescriptions: string[] = []; const colorThemeSettingSchema: IConfigurationPropertySchema = { type: 'string', description: nls.localize('colorTheme', "Specifies the color theme used in the workbench."), - default: DEFAULT_THEME_DARK_SETTING_VALUE, + default: isWeb ? DEFAULT_THEME_LIGHT_SETTING_VALUE : DEFAULT_THEME_DARK_SETTING_VALUE, enum: colorThemeSettingEnum, enumDescriptions: colorThemeSettingEnumDescriptions, errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."), @@ -60,6 +60,7 @@ const preferredHCThemeSettingSchema: IConfigurationPropertySchema = { default: DEFAULT_THEME_HC_SETTING_VALUE, enum: colorThemeSettingEnum, enumDescriptions: colorThemeSettingEnumDescriptions, + included: isWindows || isMacintosh, errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."), }; const detectColorSchemeSettingSchema: IConfigurationPropertySchema = { @@ -110,6 +111,7 @@ const themeSettingsConfiguration: IConfigurationNode = { [ThemeSettings.PRODUCT_ICON_THEME]: productIconThemeSettingSchema } }; +configurationRegistry.registerConfiguration(themeSettingsConfiguration); function tokenGroupSettings(description: string): IJSONSchema { return { @@ -231,19 +233,7 @@ export function updateProductIconThemeConfigurationSchemas(themes: IWorkbenchPro export class ThemeConfiguration { - constructor(private configurationService: IConfigurationService, themeType: ThemeType) { - switch (themeType) { - case LIGHT: - colorThemeSettingSchema.default = DEFAULT_THEME_LIGHT_SETTING_VALUE; - break; - case HIGH_CONTRAST: - colorThemeSettingSchema.default = DEFAULT_THEME_HC_SETTING_VALUE; - break; - default: - colorThemeSettingSchema.default = DEFAULT_THEME_DARK_SETTING_VALUE; - break; - } - configurationRegistry.registerConfiguration(themeSettingsConfiguration); + constructor(private configurationService: IConfigurationService) { } public get colorTheme(): string { diff --git a/src/vs/workbench/services/themes/common/workbenchThemeService.ts b/src/vs/workbench/services/themes/common/workbenchThemeService.ts index 00bd2d9f1cf..cf14976f6fa 100644 --- a/src/vs/workbench/services/themes/common/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/common/workbenchThemeService.ts @@ -78,6 +78,7 @@ export interface IWorkbenchThemeService extends IThemeService { getProductIconThemes(): Promise; onDidProductIconThemeChange: Event; + setOSHighContrast(highContrast: boolean): void; } export interface IColorCustomizations { diff --git a/src/vs/workbench/services/userData/browser/userDataInit.ts b/src/vs/workbench/services/userData/browser/userDataInit.ts new file mode 100644 index 00000000000..569affa26a9 --- /dev/null +++ b/src/vs/workbench/services/userData/browser/userDataInit.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 { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { AbstractInitializer } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { ExtensionsInitializer } from 'vs/platform/userDataSync/common/extensionsSync'; +import { GlobalStateInitializer } from 'vs/platform/userDataSync/common/globalStateSync'; +import { KeybindingsInitializer } from 'vs/platform/userDataSync/common/keybindingsSync'; +import { SettingsInitializer } from 'vs/platform/userDataSync/common/settingsSync'; +import { SnippetsInitializer } from 'vs/platform/userDataSync/common/snippetsSync'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { UserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IRequestService } from 'vs/platform/request/common/request'; +import { CONFIGURATION_SYNC_STORE_KEY, IUserDataSyncStoreClient, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { URI } from 'vs/base/common/uri'; +import { AuthenticationSessionInfo, getCurrentAuthenticationSessionInfo } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { getSyncAreaLabel } from 'vs/workbench/services/userDataSync/common/userDataSync'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { isWeb } from 'vs/base/common/platform'; + +export const IUserDataInitializationService = createDecorator('IUserDataInitializationService'); +export interface IUserDataInitializationService { + _serviceBrand: any; + + initializeRequiredResources(): Promise; + initializeOtherResources(): Promise; + initializeExtensions(instantiationService: IInstantiationService): Promise; +} + +export class UserDataInitializationService implements IUserDataInitializationService { + + _serviceBrand: any; + + private readonly initialized: SyncResource[] = []; + + constructor( + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IFileService private readonly fileService: IFileService, + @IStorageService private readonly storageService: IStorageService, + @IProductService private readonly productService: IProductService, + @IRequestService private readonly requestService: IRequestService, + @ILogService private readonly logService: ILogService + ) { } + + private _userDataSyncStoreClientPromise: Promise | undefined; + private createUserDataSyncStoreClient(): Promise { + if (!this._userDataSyncStoreClientPromise) { + this._userDataSyncStoreClientPromise = (async (): Promise => { + if (!isWeb) { + this.logService.trace(`Skipping initializing user data in desktop`); + return; + } + + if (!this.environmentService.options?.enableSyncByDefault) { + this.logService.trace(`Skipping initializing user data as sync is not enabled by default`); + return; + } + + if (!this.storageService.isNew(StorageScope.GLOBAL)) { + this.logService.trace(`Skipping initializing user data as application was opened before`); + return; + } + + if (!this.storageService.isNew(StorageScope.WORKSPACE)) { + this.logService.trace(`Skipping initializing user data as workspace was opened before`); + return; + } + + const userDataSyncStore = this.productService[CONFIGURATION_SYNC_STORE_KEY]; + if (!userDataSyncStore) { + this.logService.trace(`Skipping initializing user data as sync service is not provided`); + return; + } + + const authenticationSession = await this.getCurrentAuthenticationSessionInfo(); + + if (!authenticationSession) { + if (!this.environmentService.options?.credentialsProvider) { + this.logService.trace(`Skipping initializing user data as credentials provider is not provided`); + return; + } + this.logService.trace(`Skipping initializing user data as authentication session is not set`); + return; + } + + const userDataSyncStoreClient = new UserDataSyncStoreClient(URI.parse(userDataSyncStore.url), this.productService, this.requestService, this.logService, this.environmentService, this.fileService, this.storageService); + userDataSyncStoreClient.setAuthToken(authenticationSession.accessToken, authenticationSession.providerId); + return userDataSyncStoreClient; + })(); + } + + return this._userDataSyncStoreClientPromise; + } + + private async getCurrentAuthenticationSessionInfo(): Promise { + if (this.environmentService.options?.credentialsProvider) { + try { + const currentAuthenticationSessionInfo = await getCurrentAuthenticationSessionInfo(this.environmentService, this.productService); + if (currentAuthenticationSessionInfo) { + return currentAuthenticationSessionInfo; + } + } catch (error) { + this.logService.error(error); + return undefined; + } + } + + if (!this.environmentService.isBuilt) { + const authenticationSessionInfoElement = document.getElementById('vscode-workbench-authentication-session'); + const authenticationSessionInfoElementAttribute = authenticationSessionInfoElement ? authenticationSessionInfoElement.getAttribute('data-settings') : undefined; + if (authenticationSessionInfoElementAttribute) { + try { + return JSON.parse(authenticationSessionInfoElementAttribute); + } catch (error) { + this.logService.error(error); + return undefined; + } + } + } + + return undefined; + } + + async initializeRequiredResources(): Promise { + return this.initialize([SyncResource.Settings, SyncResource.GlobalState]); + } + + async initializeOtherResources(): Promise { + return this.initialize([SyncResource.Keybindings, SyncResource.Snippets]); + } + + async initializeExtensions(instantiationService: IInstantiationService): Promise { + return this.initialize([SyncResource.Extensions], instantiationService); + } + + private async initialize(syncResources: SyncResource[], instantiationService?: IInstantiationService): Promise { + const userDataSyncStoreClient = await this.createUserDataSyncStoreClient(); + if (!userDataSyncStoreClient) { + return; + } + + await Promise.all(syncResources.map(async syncResource => { + try { + if (this.initialized.includes(syncResource)) { + this.logService.info(`${getSyncAreaLabel(syncResource)} initialized already.`); + return; + } + this.initialized.push(syncResource); + this.logService.trace(`Initializing ${getSyncAreaLabel(syncResource)}`); + const initializer = this.createSyncResourceInitializer(syncResource, instantiationService); + const userData = await userDataSyncStoreClient.read(syncResource, null); + await initializer.initialize(userData); + this.logService.info(`Initialized ${getSyncAreaLabel(syncResource)}`); + } catch (error) { + this.logService.info(`Error while initializing ${getSyncAreaLabel(syncResource)}`); + this.logService.error(error); + } + })); + } + + private createSyncResourceInitializer(syncResource: SyncResource, instantiationService?: IInstantiationService): AbstractInitializer { + switch (syncResource) { + case SyncResource.Settings: return new SettingsInitializer(this.fileService, this.environmentService, this.logService); + case SyncResource.Keybindings: return new KeybindingsInitializer(this.fileService, this.environmentService, this.logService); + case SyncResource.Snippets: return new SnippetsInitializer(this.fileService, this.environmentService, this.logService); + case SyncResource.GlobalState: return new GlobalStateInitializer(this.storageService, this.fileService, this.environmentService, this.logService); + case SyncResource.Extensions: + if (!instantiationService) { + throw new Error('Instantiation Service is required to initialize extension'); + } + return instantiationService.createInstance(ExtensionsInitializer); + } + } + +} + +class InitializeOtherResourcesContribution implements IWorkbenchContribution { + constructor(@IUserDataInitializationService userDataInitializeService: IUserDataInitializationService) { + userDataInitializeService.initializeOtherResources(); + } +} + +if (isWeb) { + const workbenchRegistry = Registry.as(Extensions.Workbench); + workbenchRegistry.registerWorkbenchContribution(InitializeOtherResourcesContribution, LifecyclePhase.Restored); +} diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index bdac6f7a6c6..38a28f9b45b 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -11,7 +11,7 @@ import { AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/edi import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; import { flatten, equals } from 'vs/base/common/arrays'; -import { getAuthenticationProviderActivationEvent, IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { getAuthenticationProviderActivationEvent, getCurrentAuthenticationSessionInfo, IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; import { IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService, IWorkspaceStorageChangeEvent, StorageScope } from 'vs/platform/storage/common/storage'; @@ -143,18 +143,17 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat await Promise.all(unregisteredProviders.map(({ id }) => this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(id)))); } - /* wait until all providers are availabe */ - if (this._authenticationProviders.some(({ id }) => !this.authenticationService.isAuthenticationProviderRegistered(id))) { - await Event.toPromise(Event.filter(this.authenticationService.onDidRegisterAuthenticationProvider, () => this._authenticationProviders.every(({ id }) => this.authenticationService.isAuthenticationProviderRegistered(id)))); - } + /* wait until one of the providers is availabe */ + await Event.toPromise(Event.filter(this.authenticationService.onDidRegisterAuthenticationProvider, ({ id }) => this.isSupportedAuthenticationProviderId(id))); /* initialize */ await this.initialize(); } private async initialize(): Promise { - if (this.currentSessionId === undefined && this.useWorkbenchSessionId && this.environmentService.options?.authenticationSessionId) { - this.currentSessionId = this.environmentService.options.authenticationSessionId; + const authenticationSession = this.environmentService.options?.credentialsProvider ? await getCurrentAuthenticationSessionInfo(this.environmentService, this.productService) : undefined; + if (this.currentSessionId === undefined && this.useWorkbenchSessionId && (authenticationSession?.id || this.environmentService.options?.authenticationSessionId)) { + this.currentSessionId = authenticationSession?.id || this.environmentService.options?.authenticationSessionId; this.useWorkbenchSessionId = false; } diff --git a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts similarity index 76% rename from src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts rename to src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts index 35fde76ad97..d48dbbdab49 100644 --- a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts @@ -7,13 +7,12 @@ import { IUserDataAutoSyncService, UserDataSyncError, IUserDataSyncStoreManageme import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event } from 'vs/base/common/event'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger'; import { UserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService implements IUserDataAutoSyncService { +class UserDataAutoSyncService extends UserDataAutoSyncEnablementService implements IUserDataAutoSyncService { declare readonly _serviceBrand: undefined; @@ -24,16 +23,14 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i @IStorageService storageService: IStorageService, @IEnvironmentService environmentService: IEnvironmentService, @IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, - @IInstantiationService instantiationService: IInstantiationService, @ISharedProcessService sharedProcessService: ISharedProcessService, ) { super(storageService, environmentService, userDataSyncStoreManagementService); this.channel = sharedProcessService.getChannel('userDataAutoSync'); - this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(source => this.triggerSync([source], true))); } - triggerSync(sources: string[], hasToLimitSync: boolean): Promise { - return this.channel.call('triggerSync', [sources, hasToLimitSync]); + triggerSync(sources: string[], hasToLimitSync: boolean, disableCache: boolean): Promise { + return this.channel.call('triggerSync', [sources, hasToLimitSync, disableCache]); } turnOn(): Promise { @@ -45,3 +42,5 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i } } + +registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); diff --git a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSyncStoreManagementService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreManagementService.ts similarity index 87% rename from src/vs/workbench/contrib/userDataSync/electron-browser/userDataSyncStoreManagementService.ts rename to src/vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreManagementService.ts index 4a34eee9101..d408711a038 100644 --- a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSyncStoreManagementService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreManagementService.ts @@ -11,8 +11,9 @@ import { AbstractUserDataSyncStoreManagementService } from 'vs/platform/userData import { IProductService } from 'vs/platform/product/common/productService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { URI } from 'vs/base/common/uri'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -export class UserDataSyncStoreManagementService extends AbstractUserDataSyncStoreManagementService implements IUserDataSyncStoreManagementService { +class UserDataSyncStoreManagementService extends AbstractUserDataSyncStoreManagementService implements IUserDataSyncStoreManagementService { private readonly channel: IChannel; @@ -46,3 +47,5 @@ export class UserDataSyncStoreManagementService extends AbstractUserDataSyncStor } } + +registerSingleton(IUserDataSyncStoreManagementService, UserDataSyncStoreManagementService); diff --git a/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/src/vs/workbench/services/views/browser/viewDescriptorService.ts index ea89babdc73..f8c3d2107c2 100644 --- a/src/vs/workbench/services/views/browser/viewDescriptorService.ts +++ b/src/vs/workbench/services/views/browser/viewDescriptorService.ts @@ -692,7 +692,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor }); }); - this.getViewContainerModel(container).add(views.map(view => { return { viewDescriptor: view, collapsed: expandViews ? false : undefined }; })); + this.getViewContainerModel(container).add(views.map(view => { return { viewDescriptor: view, collapsed: expandViews ? false : undefined, visible: expandViews }; })); } private removeViews(container: ViewContainer, views: IViewDescriptor[]): void { diff --git a/src/vs/workbench/services/views/common/viewContainerModel.ts b/src/vs/workbench/services/views/common/viewContainerModel.ts index 60c69da9c41..a8e7d7ccee7 100644 --- a/src/vs/workbench/services/views/common/viewContainerModel.ts +++ b/src/vs/workbench/services/views/common/viewContainerModel.ts @@ -478,16 +478,16 @@ export class ViewContainerModel extends Disposable implements IViewContainerMode if (state) { // set defaults if not set if (viewDescriptor.workspace) { - state.visibleWorkspace = isUndefinedOrNull(state.visibleWorkspace) ? !viewDescriptor.hideByDefault : state.visibleWorkspace; + state.visibleWorkspace = isUndefinedOrNull(addedViewDescriptorState.visible) ? (isUndefinedOrNull(state.visibleWorkspace) ? !viewDescriptor.hideByDefault : state.visibleWorkspace) : addedViewDescriptorState.visible; } else { - state.visibleGlobal = isUndefinedOrNull(state.visibleGlobal) ? !viewDescriptor.hideByDefault : state.visibleGlobal; + state.visibleGlobal = isUndefinedOrNull(addedViewDescriptorState.visible) ? (isUndefinedOrNull(state.visibleGlobal) ? !viewDescriptor.hideByDefault : state.visibleGlobal) : addedViewDescriptorState.visible; } state.collapsed = isUndefinedOrNull(addedViewDescriptorState.collapsed) ? (isUndefinedOrNull(state.collapsed) ? !!viewDescriptor.collapsed : state.collapsed) : addedViewDescriptorState.collapsed; } else { state = { active: false, - visibleGlobal: !viewDescriptor.hideByDefault, - visibleWorkspace: !viewDescriptor.hideByDefault, + visibleGlobal: isUndefinedOrNull(addedViewDescriptorState.visible) ? !viewDescriptor.hideByDefault : addedViewDescriptorState.visible, + visibleWorkspace: isUndefinedOrNull(addedViewDescriptorState.visible) ? !viewDescriptor.hideByDefault : addedViewDescriptorState.visible, collapsed: isUndefinedOrNull(addedViewDescriptorState.collapsed) ? !!viewDescriptor.collapsed : addedViewDescriptorState.collapsed, }; } diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index ba56c701fec..e5f96509de8 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -14,6 +14,11 @@ import { ITextSnapshot } from 'vs/editor/common/model'; export const enum WorkingCopyCapabilities { + /** + * Signals no specific capability for the working copy. + */ + None = 0, + /** * Signals that the working copy requires * additional input when saving, e.g. an diff --git a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts index 50fbeff1806..97840f97145 100644 --- a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IWorkingCopy, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { URI } from 'vs/base/common/uri'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -23,7 +23,7 @@ export class TestWorkingCopy extends Disposable implements IWorkingCopy { private readonly _onDispose = this._register(new Emitter()); readonly onDispose = this._onDispose.event; - readonly capabilities = 0; + readonly capabilities = WorkingCopyCapabilities.None; readonly name = basename(this.resource); diff --git a/src/vs/workbench/test/browser/api/extHostDocumentData.test.ts b/src/vs/workbench/test/browser/api/extHostDocumentData.test.ts index 93dead3f3df..ef2d1c5c0aa 100644 --- a/src/vs/workbench/test/browser/api/extHostDocumentData.test.ts +++ b/src/vs/workbench/test/browser/api/extHostDocumentData.test.ts @@ -35,7 +35,7 @@ suite('ExtHostDocumentData', () => { 'and this is line number two', //27 'it is followed by #3', //20 'and finished with the fourth.', //29 - ], '\n', 'text', 1, false); + ], '\n', 1, 'text', false); }); test('readonly-ness', () => { @@ -55,7 +55,7 @@ suite('ExtHostDocumentData', () => { saved = uri; return Promise.resolve(true); } - }, URI.parse('foo:bar'), [], '\n', 'text', 1, true); + }, URI.parse('foo:bar'), [], '\n', 1, 'text', true); return data.document.save().then(() => { assert.equal(saved.toString(), 'foo:bar'); @@ -242,7 +242,7 @@ suite('ExtHostDocumentData', () => { test('getWordRangeAtPosition', () => { data = new ExtHostDocumentData(undefined!, URI.file(''), [ 'aaaa bbbb+cccc abc' - ], '\n', 'text', 1, false); + ], '\n', 1, 'text', false); let range = data.document.getWordRangeAtPosition(new Position(0, 2))!; assert.equal(range.start.line, 0); @@ -276,7 +276,7 @@ suite('ExtHostDocumentData', () => { 'function() {', ' "far boo"', '}' - ], '\n', 'text', 1, false); + ], '\n', 1, 'text', false); let range = data.document.getWordRangeAtPosition(new Position(0, 0), /\/\*.+\*\//); assert.equal(range, undefined); @@ -304,7 +304,7 @@ suite('ExtHostDocumentData', () => { data = new ExtHostDocumentData(undefined!, URI.file(''), [ perfData._$_$_expensive - ], '\n', 'text', 1, false); + ], '\n', 1, 'text', false); let range = data.document.getWordRangeAtPosition(new Position(0, 1_177_170), regex)!; assert.equal(range, undefined); @@ -323,7 +323,7 @@ suite('ExtHostDocumentData', () => { data = new ExtHostDocumentData(undefined!, URI.file(''), [ line - ], '\n', 'text', 1, false); + ], '\n', 1, 'text', false); let range = data.document.getWordRangeAtPosition(new Position(0, 27), regex)!; assert.equal(range.start.line, 0); @@ -387,7 +387,7 @@ suite('ExtHostDocumentData updates line mapping', () => { } function testLineMappingDirectionAfterEvents(lines: string[], eol: string, direction: AssertDocumentLineMappingDirection, e: IModelChangedEvent): void { - let myDocument = new ExtHostDocumentData(undefined!, URI.file(''), lines.slice(0), eol, 'text', 1, false); + let myDocument = new ExtHostDocumentData(undefined!, URI.file(''), lines.slice(0), eol, 1, 'text', false); assertDocumentLineMapping(myDocument, direction); myDocument.onEvents(e); diff --git a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts index 4f4d4967242..70070cd869b 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts @@ -4,148 +4,249 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import * as vscode from 'vscode'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { TestRPCProtocol } from 'vs/workbench/test/browser/api/testRPCProtocol'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { NullLogService } from 'vs/platform/log/common/log'; import { mock } from 'vs/base/test/common/mock'; -import { MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostNotebookDocument, ExtHostCell } from 'vs/workbench/api/common/extHostNotebook'; -import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IModelAddedData, MainContext, MainThreadCommandsShape, MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostNotebookDocument, ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; +import { CellKind, CellUri, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; +import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { isEqual } from 'vs/base/common/resources'; +import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; +import { generateUuid } from 'vs/base/common/uuid'; + +suite('NotebookCell#Document', function () { -suite('NotebookCell', function () { let rpcProtocol: TestRPCProtocol; + let notebook: ExtHostNotebookDocument; let extHostDocumentsAndEditors: ExtHostDocumentsAndEditors; - + let extHostDocuments: ExtHostDocuments; + let extHostNotebooks: ExtHostNotebookController; + const notebookUri = URI.parse('test:///notebook.file'); const disposables = new DisposableStore(); - const fakeNotebookProxy = new class extends mock() { }; - const fakeNotebook = new class extends mock() { }; setup(async function () { disposables.clear(); + rpcProtocol = new TestRPCProtocol(); + rpcProtocol.set(MainContext.MainThreadCommands, new class extends mock() { + $registerCommand() { } + }); + rpcProtocol.set(MainContext.MainThreadNotebook, new class extends mock() { + async $registerNotebookProvider() { } + async $unregisterNotebookProvider() { } + }); extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); - }); - - test('Document is real', function () { - - const dto = { - cellKind: CellKind.Code, - eol: '\n', - source: ['aaaa', 'bbbb', 'cccc'], - handle: 0, - language: 'fooLang', - outputs: [], - uri: URI.parse('test:/path') + extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); + const extHostStoragePaths = new class extends mock() { + workspaceValue() { + return URI.from({ scheme: 'test', path: generateUuid() }); + } }; - const cell = new ExtHostCell(fakeNotebookProxy, fakeNotebook, extHostDocumentsAndEditors, dto); - - assert.ok(cell.document); - assert.strictEqual(cell.document.version, 0); - assert.strictEqual(cell.document.languageId, dto.language); - assert.strictEqual(cell.document.uri.toString(), dto.uri.toString()); - assert.strictEqual(cell.uri.toString(), dto.uri.toString()); - }); - - - test('Document is uses actual document when possible', function () { - - const dto = { - cellKind: CellKind.Code, - eol: '\n', - source: ['aaaa', 'bbbb', 'cccc'], - handle: 0, - language: 'fooLang', - outputs: [], - uri: URI.parse('test:/path') - }; - const cell = new ExtHostCell(fakeNotebookProxy, fakeNotebook, extHostDocumentsAndEditors, dto); - - // this is the "default document" which is used when the real - // document isn't open - const documentNow = cell.document; - - extHostDocumentsAndEditors.$acceptDocumentsAndEditorsDelta({ + extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors, { isExtensionDevelopmentDebug: false, webviewCspSource: '', webviewResourceRoot: '' }, new NullLogService(), extHostStoragePaths); + let reg = extHostNotebooks.registerNotebookContentProvider(nullExtensionDescription, 'test', new class extends mock() { + // async openNotebook() { } + }); + extHostNotebooks.$acceptDocumentAndEditorsDelta({ addedDocuments: [{ - isDirty: false, - versionId: 12, - modeId: dto.language, - uri: dto.uri, - lines: dto.source, - EOL: dto.eol + handle: 0, + uri: notebookUri, + viewType: 'test', + versionId: 0, + cells: [{ + handle: 0, + uri: CellUri.generate(notebookUri, 0), + source: ['### Heading'], + eol: '\n', + language: 'markdown', + cellKind: CellKind.Markdown, + outputs: [], + }, { + handle: 1, + uri: CellUri.generate(notebookUri, 1), + source: ['console.log("aaa")', 'console.log("bbb")'], + eol: '\n', + language: 'javascript', + cellKind: CellKind.Code, + outputs: [], + }], + }], + addedEditors: [{ + documentUri: notebookUri, + id: '_notebook_editor_0', + selections: [0], + visibleRanges: [] }] }); + extHostNotebooks.$acceptDocumentAndEditorsDelta({ newActiveEditor: '_notebook_editor_0' }); - // the real document - assert.ok(documentNow !== cell.document); - assert.strictEqual(cell.document.languageId, dto.language); - assert.strictEqual(cell.document.uri.toString(), dto.uri.toString()); - assert.strictEqual(cell.uri.toString(), dto.uri.toString()); + notebook = extHostNotebooks.notebookDocuments[0]!; - // back to "default document" - extHostDocumentsAndEditors.$acceptDocumentsAndEditorsDelta({ removedDocuments: [dto.uri] }); - assert.ok(documentNow === cell.document); - }); - - test('Document can change language (1/2)', function () { - - const dto = { - cellKind: CellKind.Code, - eol: '\n', - source: ['aaaa', 'bbbb', 'cccc'], - handle: 0, - language: 'fooLang', - outputs: [], - uri: URI.parse('test:/path') - }; - const cell = new ExtHostCell(fakeNotebookProxy, fakeNotebook, extHostDocumentsAndEditors, dto); - - assert.strictEqual(cell.document.languageId, dto.language); - cell.defaultDocument._acceptLanguageId('barLang'); - assert.strictEqual(cell.document.languageId, 'barLang'); + disposables.add(reg); + disposables.add(notebook); + disposables.add(extHostDocuments); }); - test('Document can change language (1/2)', function () { + test('cell document is vscode.TextDocument', async function () { + assert.strictEqual(notebook.notebookDocument.cells.length, 2); - const dto = { - cellKind: CellKind.Code, - eol: '\n', - source: ['aaaa', 'bbbb', 'cccc'], - handle: 0, - language: 'fooLang', - outputs: [], - uri: URI.parse('test:/path') - }; + const [c1, c2] = notebook.notebookDocument.cells; + const d1 = extHostDocuments.getDocument(c1.uri); - extHostDocumentsAndEditors.$acceptDocumentsAndEditorsDelta({ - addedDocuments: [{ - isDirty: false, - versionId: 12, - modeId: dto.language, - uri: dto.uri, - lines: dto.source, - EOL: dto.eol - }] + assert.ok(d1); + assert.equal(d1.languageId, c1.language); + assert.equal(d1.version, 1); + assert.ok(d1.notebook === notebook.notebookDocument); + + const d2 = extHostDocuments.getDocument(c2.uri); + assert.ok(d2); + assert.equal(d2.languageId, c2.language); + assert.equal(d2.version, 1); + assert.ok(d2.notebook === notebook.notebookDocument); + }); + + test('cell document goes when notebook closes', async function () { + const cellUris: string[] = []; + for (let cell of notebook.notebookDocument.cells) { + assert.ok(extHostDocuments.getDocument(cell.uri)); + cellUris.push(cell.uri.toString()); + } + + const removedCellUris: string[] = []; + const reg = extHostDocuments.onDidRemoveDocument(doc => { + removedCellUris.push(doc.uri.toString()); }); - const extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); + extHostNotebooks.$acceptDocumentAndEditorsDelta({ removedDocuments: [notebook.uri] }); + reg.dispose(); - const cell = new ExtHostCell(fakeNotebookProxy, fakeNotebook, extHostDocumentsAndEditors, dto); - - // a real document already exists and therefore - // the "default document" doesn't count - - assert.strictEqual(cell.document.languageId, dto.language); - cell.defaultDocument._acceptLanguageId('barLang'); - assert.strictEqual(cell.document.languageId, dto.language); - - extHostDocuments.$acceptModelModeChanged(dto.uri, dto.language, 'barLang'); - assert.strictEqual(cell.document.languageId, 'barLang'); + assert.strictEqual(removedCellUris.length, 2); + assert.deepStrictEqual(removedCellUris.sort(), cellUris.sort()); }); + test('cell document is vscode.TextDocument after changing it', async function () { + + const p = new Promise((resolve, reject) => { + extHostNotebooks.onDidChangeNotebookCells(e => { + try { + assert.strictEqual(e.changes.length, 1); + assert.strictEqual(e.changes[0].items.length, 2); + + const [first, second] = e.changes[0].items; + + const doc1 = extHostDocuments.getAllDocumentData().find(data => isEqual(data.document.uri, first.uri)); + assert.ok(doc1); + assert.strictEqual(doc1?.document === first.document, true); + + const doc2 = extHostDocuments.getAllDocumentData().find(data => isEqual(data.document.uri, second.uri)); + assert.ok(doc2); + assert.strictEqual(doc2?.document === second.document, true); + + resolve(); + + } catch (err) { + reject(err); + } + }); + }); + + extHostNotebooks.$acceptModelChanged(notebookUri, { + kind: NotebookCellsChangeType.ModelChange, + versionId: notebook.notebookDocument.version + 1, + changes: [[0, 0, [{ + handle: 2, + uri: CellUri.generate(notebookUri, 2), + source: ['Hello', 'World', 'Hello World!'], + eol: '\n', + language: 'test', + cellKind: CellKind.Code, + outputs: [], + }, { + handle: 3, + uri: CellUri.generate(notebookUri, 3), + source: ['Hallo', 'Welt', 'Hallo Welt!'], + eol: '\n', + language: 'test', + cellKind: CellKind.Code, + outputs: [], + }]]] + }, false); + + await p; + + }); + + test('cell document stays open when notebook is still open', async function () { + + const docs: vscode.TextDocument[] = []; + const addData: IModelAddedData[] = []; + for (let cell of notebook.notebookDocument.cells) { + const doc = extHostDocuments.getDocument(cell.uri); + assert.ok(doc); + assert.equal(extHostDocuments.getDocument(cell.uri).isClosed, false); + docs.push(doc); + addData.push({ + EOL: '\n', + isDirty: doc.isDirty, + lines: doc.getText().split('\n'), + modeId: doc.languageId, + uri: doc.uri, + versionId: doc.version + }); + } + + // this call happens when opening a document on the main side + extHostDocumentsAndEditors.$acceptDocumentsAndEditorsDelta({ addedDocuments: addData }); + + // this call happens when closing a document from the main side + extHostDocumentsAndEditors.$acceptDocumentsAndEditorsDelta({ removedDocuments: docs.map(d => d.uri) }); + + // notebook is still open -> cell documents stay open + for (let cell of notebook.notebookDocument.cells) { + assert.ok(extHostDocuments.getDocument(cell.uri)); + assert.equal(extHostDocuments.getDocument(cell.uri).isClosed, false); + } + + // close notebook -> docs are closed + extHostNotebooks.$acceptDocumentAndEditorsDelta({ removedDocuments: [notebook.uri] }); + for (let cell of notebook.notebookDocument.cells) { + assert.throws(() => extHostDocuments.getDocument(cell.uri)); + } + for (let doc of docs) { + assert.equal(doc.isClosed, true); + } + }); + + test('cell document goes when cell is removed', async function () { + + assert.equal(notebook.notebookDocument.cells.length, 2); + const [cell1, cell2] = notebook.notebookDocument.cells; + + extHostNotebooks.$acceptModelChanged(notebook.uri, { + kind: NotebookCellsChangeType.ModelChange, + versionId: 2, + changes: [[0, 1, []]] + }, false); + + assert.equal(notebook.notebookDocument.cells.length, 1); + assert.equal(cell1.document.isClosed, true); // ref still alive! + assert.equal(cell2.document.isClosed, false); + + assert.throws(() => extHostDocuments.getDocument(cell1.uri)); + }); + + test('cell document knows notebook', function () { + for (let cells of notebook.notebookDocument.cells) { + assert.equal(cells.document.notebook === notebook.notebookDocument, true); + } + }); }); diff --git a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts index 3fcaa6da684..f2ca94c32d5 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts @@ -19,7 +19,8 @@ import * as vscode from 'vscode'; import { mock } from 'vs/workbench/test/common/workbenchTestServices'; import { MainContext, MainThreadCommandsShape, MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; import { DisposableStore } from 'vs/base/common/lifecycle'; - +import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; +import { generateUuid } from 'vs/base/common/uuid'; suite('NotebookConcatDocument', function () { @@ -44,11 +45,16 @@ suite('NotebookConcatDocument', function () { }); extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); - extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors, { isExtensionDevelopmentDebug: false, webviewCspSource: '', webviewResourceRoot: '' }); + const extHostStoragePaths = new class extends mock() { + workspaceValue() { + return URI.from({ scheme: 'test', path: generateUuid() }); + } + }; + extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors, { isExtensionDevelopmentDebug: false, webviewCspSource: '', webviewResourceRoot: '' }, new NullLogService(), extHostStoragePaths); let reg = extHostNotebooks.registerNotebookContentProvider(nullExtensionDescription, 'test', new class extends mock() { // async openNotebook() { } }); - await extHostNotebooks.$acceptDocumentAndEditorsDelta({ + extHostNotebooks.$acceptDocumentAndEditorsDelta({ addedDocuments: [{ handle: 0, uri: notebookUri, @@ -68,11 +74,12 @@ suite('NotebookConcatDocument', function () { { documentUri: notebookUri, id: '_notebook_editor_0', - selections: [0] + selections: [0], + visibleRanges: [] } ] }); - await extHostNotebooks.$acceptDocumentAndEditorsDelta({ newActiveEditor: '_notebook_editor_0' }); + extHostNotebooks.$acceptDocumentAndEditorsDelta({ newActiveEditor: '_notebook_editor_0' }); notebook = extHostNotebooks.notebookDocuments[0]!; @@ -82,7 +89,7 @@ suite('NotebookConcatDocument', function () { }); test('empty', function () { - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assert.equal(doc.getText(), ''); assert.equal(doc.version, 0); @@ -119,7 +126,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: cellUri1, @@ -137,12 +144,12 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); + }, false); - assert.equal(notebook.cells.length, 1 + 2); // markdown and code + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assert.equal(doc.contains(cellUri1), true); assert.equal(doc.contains(cellUri2), true); @@ -153,7 +160,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -171,30 +178,30 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); + }, false); - assert.equal(notebook.cells.length, 1 + 2); // markdown and code + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); - assertLocation(doc, new Position(4, 0), new Location(notebook.cells[1].uri, new Position(1, 0))); - assertLocation(doc, new Position(4, 3), new Location(notebook.cells[1].uri, new Position(1, 3))); - assertLocation(doc, new Position(5, 11), new Location(notebook.cells[1].uri, new Position(2, 11))); - assertLocation(doc, new Position(5, 12), new Location(notebook.cells[1].uri, new Position(2, 11)), false); // don't check identity because position will be clamped + assertLocation(doc, new Position(0, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(4, 0), new Location(notebook.notebookDocument.cells[1].uri, new Position(1, 0))); + assertLocation(doc, new Position(4, 3), new Location(notebook.notebookDocument.cells[1].uri, new Position(1, 3))); + assertLocation(doc, new Position(5, 11), new Location(notebook.notebookDocument.cells[1].uri, new Position(2, 11))); + assertLocation(doc, new Position(5, 12), new Location(notebook.notebookDocument.cells[1].uri, new Position(2, 11)), false); // don't check identity because position will be clamped }); test('location, position mapping, cell changes', function () { - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); // UPDATE 1 extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -204,20 +211,20 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); - assert.equal(notebook.cells.length, 1 + 1); + }, false); + assert.equal(notebook.notebookDocument.cells.length, 1 + 1); assert.equal(doc.version, 1); assertLines(doc, 'Hello', 'World', 'Hello World!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); - assertLocation(doc, new Position(2, 2), new Location(notebook.cells[0].uri, new Position(2, 2))); - assertLocation(doc, new Position(4, 0), new Location(notebook.cells[0].uri, new Position(2, 12)), false); // clamped + assertLocation(doc, new Position(0, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(2, 2), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 2))); + assertLocation(doc, new Position(4, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 12)), false); // clamped // UPDATE 2 extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[1, 0, [{ handle: 2, uri: CellUri.generate(notebook.uri, 2), @@ -227,39 +234,39 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); + }, false); - assert.equal(notebook.cells.length, 1 + 2); + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); assert.equal(doc.version, 2); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); - assertLocation(doc, new Position(4, 0), new Location(notebook.cells[1].uri, new Position(1, 0))); - assertLocation(doc, new Position(4, 3), new Location(notebook.cells[1].uri, new Position(1, 3))); - assertLocation(doc, new Position(5, 11), new Location(notebook.cells[1].uri, new Position(2, 11))); - assertLocation(doc, new Position(5, 12), new Location(notebook.cells[1].uri, new Position(2, 11)), false); // don't check identity because position will be clamped + assertLocation(doc, new Position(0, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(4, 0), new Location(notebook.notebookDocument.cells[1].uri, new Position(1, 0))); + assertLocation(doc, new Position(4, 3), new Location(notebook.notebookDocument.cells[1].uri, new Position(1, 3))); + assertLocation(doc, new Position(5, 11), new Location(notebook.notebookDocument.cells[1].uri, new Position(2, 11))); + assertLocation(doc, new Position(5, 12), new Location(notebook.notebookDocument.cells[1].uri, new Position(2, 11)), false); // don't check identity because position will be clamped // UPDATE 3 (remove cell #2 again) extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[1, 1, []]] - }); - assert.equal(notebook.cells.length, 1 + 1); + }, false); + assert.equal(notebook.notebookDocument.cells.length, 1 + 1); assert.equal(doc.version, 3); assertLines(doc, 'Hello', 'World', 'Hello World!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); - assertLocation(doc, new Position(2, 2), new Location(notebook.cells[0].uri, new Position(2, 2))); - assertLocation(doc, new Position(4, 0), new Location(notebook.cells[0].uri, new Position(2, 12)), false); // clamped + assertLocation(doc, new Position(0, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(2, 2), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 2))); + assertLocation(doc, new Position(4, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 12)), false); // clamped }); test('location, position mapping, cell-document changes', function () { - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); // UPDATE 1 extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -277,33 +284,22 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); - assert.equal(notebook.cells.length, 1 + 2); + }, false); + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); assert.equal(doc.version, 1); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); - assertLocation(doc, new Position(2, 2), new Location(notebook.cells[0].uri, new Position(2, 2))); - assertLocation(doc, new Position(2, 12), new Location(notebook.cells[0].uri, new Position(2, 12))); - assertLocation(doc, new Position(4, 0), new Location(notebook.cells[1].uri, new Position(1, 0))); - assertLocation(doc, new Position(4, 3), new Location(notebook.cells[1].uri, new Position(1, 3))); + assertLocation(doc, new Position(0, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(2, 2), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 2))); + assertLocation(doc, new Position(2, 12), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 12))); + assertLocation(doc, new Position(4, 0), new Location(notebook.notebookDocument.cells[1].uri, new Position(1, 0))); + assertLocation(doc, new Position(4, 3), new Location(notebook.notebookDocument.cells[1].uri, new Position(1, 3))); // offset math let cell1End = doc.offsetAt(new Position(2, 12)); assert.equal(doc.positionAt(cell1End).isEqual(new Position(2, 12)), true); - extHostDocumentsAndEditors.$acceptDocumentsAndEditorsDelta({ - addedDocuments: [{ - uri: notebook.cells[0].uri, - versionId: 1, - lines: ['Hello', 'World', 'Hello World!'], - EOL: '\n', - modeId: '', - isDirty: false - }] - }); - - extHostDocuments.$acceptModelChanged(notebook.cells[0].uri, { + extHostDocuments.$acceptModelChanged(notebook.notebookDocument.cells[0].uri, { versionId: 0, eol: '\n', changes: [{ @@ -314,7 +310,7 @@ suite('NotebookConcatDocument', function () { }] }, false); assertLines(doc, 'Hello', 'World', 'Hi World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(2, 12), new Location(notebook.cells[0].uri, new Position(2, 9)), false); + assertLocation(doc, new Position(2, 12), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 9)), false); assert.equal(doc.positionAt(cell1End).isEqual(new Position(3, 2)), true); @@ -324,7 +320,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -342,11 +338,11 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); + }, false); - const mixedDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); - const fooLangDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, 'fooLang'); - const barLangDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, 'barLang'); + const mixedDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); + const fooLangDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, 'fooLang'); + const barLangDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, 'barLang'); assertLines(mixedDoc, 'fooLang-document', 'barLang-document'); assertLines(fooLangDoc, 'fooLang-document'); @@ -354,7 +350,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[2, 0, [{ handle: 3, uri: CellUri.generate(notebook.uri, 3), @@ -364,7 +360,7 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); + }, false); assertLines(mixedDoc, 'fooLang-document', 'barLang-document', 'barLang-document2'); assertLines(fooLangDoc, 'fooLang-document'); @@ -388,7 +384,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -406,11 +402,11 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); + }, false); - assert.equal(notebook.cells.length, 1 + 2); // markdown and code + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); assertOffsetAtPosition(doc, 0, { line: 0, character: 0 }); @@ -441,7 +437,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -459,26 +455,26 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); + }, false); - assert.equal(notebook.cells.length, 1 + 2); // markdown and code + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocationAtPosition(doc, { line: 0, character: 0 }, { uri: notebook.cells[0].uri, line: 0, character: 0 }); - assertLocationAtPosition(doc, { line: 2, character: 0 }, { uri: notebook.cells[0].uri, line: 2, character: 0 }); - assertLocationAtPosition(doc, { line: 2, character: 12 }, { uri: notebook.cells[0].uri, line: 2, character: 12 }); - assertLocationAtPosition(doc, { line: 3, character: 0 }, { uri: notebook.cells[1].uri, line: 0, character: 0 }); - assertLocationAtPosition(doc, { line: 5, character: 0 }, { uri: notebook.cells[1].uri, line: 2, character: 0 }); - assertLocationAtPosition(doc, { line: 5, character: 11 }, { uri: notebook.cells[1].uri, line: 2, character: 11 }); + assertLocationAtPosition(doc, { line: 0, character: 0 }, { uri: notebook.notebookDocument.cells[0].uri, line: 0, character: 0 }); + assertLocationAtPosition(doc, { line: 2, character: 0 }, { uri: notebook.notebookDocument.cells[0].uri, line: 2, character: 0 }); + assertLocationAtPosition(doc, { line: 2, character: 12 }, { uri: notebook.notebookDocument.cells[0].uri, line: 2, character: 12 }); + assertLocationAtPosition(doc, { line: 3, character: 0 }, { uri: notebook.notebookDocument.cells[1].uri, line: 0, character: 0 }); + assertLocationAtPosition(doc, { line: 5, character: 0 }, { uri: notebook.notebookDocument.cells[1].uri, line: 2, character: 0 }); + assertLocationAtPosition(doc, { line: 5, character: 11 }, { uri: notebook.notebookDocument.cells[1].uri, line: 2, character: 11 }); }); test('getText(range)', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -496,11 +492,11 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); + }, false); - assert.equal(notebook.cells.length, 1 + 2); // markdown and code + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); assert.equal(doc.getText(new Range(0, 0, 0, 0)), ''); @@ -512,7 +508,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -530,11 +526,11 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); + }, false); - assert.equal(notebook.cells.length, 1 + 2); // markdown and code + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); diff --git a/src/vs/workbench/test/browser/api/extHostTextEditor.test.ts b/src/vs/workbench/test/browser/api/extHostTextEditor.test.ts index ddfaa9b86f6..dc844a5273a 100644 --- a/src/vs/workbench/test/browser/api/extHostTextEditor.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTextEditor.test.ts @@ -17,7 +17,7 @@ suite('ExtHostTextEditor', () => { let editor: ExtHostTextEditor; let doc = new ExtHostDocumentData(undefined!, URI.file(''), [ 'aaaa bbbb+cccc abc' - ], '\n', 'text', 1, false); + ], '\n', 1, 'text', false); setup(() => { editor = new ExtHostTextEditor('fake', null!, new NullLogService(), doc, [], { cursorStyle: 0, insertSpaces: true, lineNumbers: 1, tabSize: 4, indentSize: 4 }, [], 1); diff --git a/src/vs/workbench/test/browser/api/extHostTextEditors.test.ts b/src/vs/workbench/test/browser/api/extHostTextEditors.test.ts index 4d4a90b1ec1..a1778a00358 100644 --- a/src/vs/workbench/test/browser/api/extHostTextEditors.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTextEditors.test.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import { MainContext, MainThreadTextEditorsShape, IWorkspaceEditDto } from 'vs/workbench/api/common/extHost.protocol'; +import { MainContext, MainThreadTextEditorsShape, IWorkspaceEditDto, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { SingleProxyRPCProtocol, TestRPCProtocol } from 'vs/workbench/test/browser/api/testRPCProtocol'; import { ExtHostEditors } from 'vs/workbench/api/common/extHostTextEditors'; -import { WorkspaceTextEdit } from 'vs/editor/common/modes'; import { NullLogService } from 'vs/platform/log/common/log'; +import { assertType } from 'vs/base/common/types'; suite('ExtHostTextEditors.applyWorkspaceEdit', () => { @@ -40,7 +40,7 @@ suite('ExtHostTextEditors.applyWorkspaceEdit', () => { EOL: '\n', }] }); - editors = new ExtHostEditors(rpcProtocol, documentsAndEditors); + editors = new ExtHostEditors(rpcProtocol, documentsAndEditors, null!); }); test('uses version id if document available', async () => { @@ -48,7 +48,9 @@ suite('ExtHostTextEditors.applyWorkspaceEdit', () => { edit.replace(resource, new extHostTypes.Range(0, 0, 0, 0), 'hello'); await editors.applyWorkspaceEdit(edit); assert.equal(workspaceResourceEdits.edits.length, 1); - assert.equal((workspaceResourceEdits.edits[0]).modelVersionId, 1337); + const [first] = workspaceResourceEdits.edits; + assertType(first._type === WorkspaceEditType.Text); + assert.equal(first.modelVersionId, 1337); }); test('does not use version id if document is not available', async () => { @@ -56,7 +58,9 @@ suite('ExtHostTextEditors.applyWorkspaceEdit', () => { edit.replace(URI.parse('foo:bar2'), new extHostTypes.Range(0, 0, 0, 0), 'hello'); await editors.applyWorkspaceEdit(edit); assert.equal(workspaceResourceEdits.edits.length, 1); - assert.ok(typeof (workspaceResourceEdits.edits[0]).modelVersionId === 'undefined'); + const [first] = workspaceResourceEdits.edits; + assertType(first._type === WorkspaceEditType.Text); + assert.ok(typeof first.modelVersionId === 'undefined'); }); }); diff --git a/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts b/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts index 69494b5d834..9ba5a4e9f1b 100644 --- a/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts @@ -203,7 +203,8 @@ suite('ExtHostTreeView', function () { assert.deepEqual(actuals, ['1/a', '1/b']); return testObject.$getChildren('testNodeWithIdTreeProvider', '1/a') .then(() => testObject.$getChildren('testNodeWithIdTreeProvider', '1/b')) - .then(() => { assert.fail('Should fail with duplicate id'); done(); }, () => done()); + .then(() => assert.fail('Should fail with duplicate id')) + .finally(done); }); }); onDidChangeTreeNode.fire(undefined); diff --git a/src/vs/workbench/test/browser/api/extHostTypeConverter.test.ts b/src/vs/workbench/test/browser/api/extHostTypeConverter.test.ts index a14517bfff2..feb4e1dd1ed 100644 --- a/src/vs/workbench/test/browser/api/extHostTypeConverter.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTypeConverter.test.ts @@ -45,7 +45,7 @@ suite('ExtHostTypeConverter', function () { data = MarkdownString.from('hello@foo.bar'); assert.equal(data.value, 'hello@foo.bar'); assert.equal(size(data.uris!), 1); - assert.ok(!!data.uris!['mailto:hello@foo.bar']); + // assert.ok(!!data.uris!['mailto:hello@foo.bar']); data = MarkdownString.from('*hello* [click](command:me)'); assert.equal(data.value, '*hello* [click](command:me)'); diff --git a/src/vs/workbench/test/browser/api/extHostTypes.test.ts b/src/vs/workbench/test/browser/api/extHostTypes.test.ts index 92e9616c1a8..08e6159df75 100644 --- a/src/vs/workbench/test/browser/api/extHostTypes.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTypes.test.ts @@ -384,21 +384,21 @@ suite('ExtHostTypes', function () { edit.replace(URI.parse('foo:a'), new types.Range(2, 1, 2, 1), 'bar'); edit.replace(URI.parse('foo:b'), new types.Range(3, 1, 3, 1), 'bazz'); - const all = edit.allEntries(); + const all = edit._allEntries(); assert.equal(all.length, 4); const [first, second, third, fourth] = all; - assertType(first._type === 2); + assertType(first._type === types.FileEditType.Text); assert.equal(first.uri.toString(), 'foo:a'); - assertType(second._type === 1); + assertType(second._type === types.FileEditType.File); assert.equal(second.from!.toString(), 'foo:a'); assert.equal(second.to!.toString(), 'foo:b'); - assertType(third._type === 2); + assertType(third._type === types.FileEditType.Text); assert.equal(third.uri.toString(), 'foo:a'); - assertType(fourth._type === 2); + assertType(fourth._type === types.FileEditType.Text); assert.equal(fourth.uri.toString(), 'foo:b'); }); @@ -408,11 +408,11 @@ suite('ExtHostTypes', function () { edit.insert(uri, new types.Position(0, 0), 'Hello'); edit.insert(uri, new types.Position(0, 0), 'Foo'); - assert.equal(edit.allEntries().length, 2); - let [first, second] = edit.allEntries(); + assert.equal(edit._allEntries().length, 2); + let [first, second] = edit._allEntries(); - assertType(first._type === 2); - assertType(second._type === 2); + assertType(first._type === types.FileEditType.Text); + assertType(second._type === types.FileEditType.Text); assert.equal(first.edit.newText, 'Hello'); assert.equal(second.edit.newText, 'Foo'); }); diff --git a/src/vs/workbench/test/browser/api/extHostWebview.test.ts b/src/vs/workbench/test/browser/api/extHostWebview.test.ts index f74b2998d27..577b61c1747 100644 --- a/src/vs/workbench/test/browser/api/extHostWebview.test.ts +++ b/src/vs/workbench/test/browser/api/extHostWebview.test.ts @@ -3,33 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type * as vscode from 'vscode'; import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; +import { mock } from 'vs/base/test/common/mock'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { NullLogService } from 'vs/platform/log/common/log'; -import { MainThreadWebviews } from 'vs/workbench/api/browser/mainThreadWebview'; -import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; -import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; -import { mock } from 'vs/base/test/common/mock'; -import { SingleProxyRPCProtocol } from './testRPCProtocol'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; -import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { MainThreadWebviewManager } from 'vs/workbench/api/browser/mainThreadWebviewManager'; import { IExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; import { NullApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; +import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; +import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; +import type * as vscode from 'vscode'; +import { SingleProxyRPCProtocol } from './testRPCProtocol'; suite('ExtHostWebview', () => { let rpcProtocol: (IExtHostRpcService & IExtHostContext) | undefined; - let extHostDocuments: ExtHostDocuments | undefined; setup(() => { const shape = createNoopMainThreadWebviews(); rpcProtocol = SingleProxyRPCProtocol(shape); - - const extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); - extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); }); test('Cannot register multiple serializers for the same view type', async () => { @@ -39,7 +34,9 @@ suite('ExtHostWebview', () => { webviewCspSource: '', webviewResourceRoot: '', isExtensionDevelopmentDebug: false, - }, undefined, new NullLogService(), NullApiDeprecationService, extHostDocuments!); + }, undefined, new NullLogService(), NullApiDeprecationService); + + const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); let lastInvokedDeserializer: vscode.WebviewPanelSerializer | undefined = undefined; @@ -54,20 +51,20 @@ suite('ExtHostWebview', () => { const serializerA = new NoopSerializer(); const serializerB = new NoopSerializer(); - const serializerARegistration = extHostWebviews.registerWebviewPanelSerializer(extension, viewType, serializerA); + const serializerARegistration = extHostWebviewPanels.registerWebviewPanelSerializer(extension, viewType, serializerA); - await extHostWebviews.$deserializeWebviewPanel('x', viewType, 'title', {}, 0 as EditorViewColumn, {}); + await extHostWebviewPanels.$deserializeWebviewPanel('x', viewType, 'title', {}, 0 as EditorViewColumn, {}); assert.strictEqual(lastInvokedDeserializer, serializerA); assert.throws( - () => extHostWebviews.registerWebviewPanelSerializer(extension, viewType, serializerB), + () => extHostWebviewPanels.registerWebviewPanelSerializer(extension, viewType, serializerB), 'Should throw when registering two serializers for the same view'); serializerARegistration.dispose(); - extHostWebviews.registerWebviewPanelSerializer(extension, viewType, serializerB); + extHostWebviewPanels.registerWebviewPanelSerializer(extension, viewType, serializerB); - await extHostWebviews.$deserializeWebviewPanel('x', viewType, 'title', {}, 0 as EditorViewColumn, {}); + await extHostWebviewPanels.$deserializeWebviewPanel('x', viewType, 'title', {}, 0 as EditorViewColumn, {}); assert.strictEqual(lastInvokedDeserializer, serializerB); }); @@ -76,8 +73,11 @@ suite('ExtHostWebview', () => { webviewCspSource: '', webviewResourceRoot: 'vscode-resource://{{resource}}', isExtensionDevelopmentDebug: false, - }, undefined, new NullLogService(), NullApiDeprecationService, extHostDocuments!); - const webview = extHostWebviews.createWebviewPanel({} as any, 'type', 'title', 1, {}); + }, undefined, new NullLogService(), NullApiDeprecationService); + + const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); + + const webview = extHostWebviewPanels.createWebviewPanel({} as any, 'type', 'title', 1, {}); assert.strictEqual( webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString(), @@ -115,8 +115,11 @@ suite('ExtHostWebview', () => { webviewCspSource: '', webviewResourceRoot: `https://{{uuid}}.webview.contoso.com/commit/{{resource}}`, isExtensionDevelopmentDebug: false, - }, undefined, new NullLogService(), NullApiDeprecationService, extHostDocuments!); - const webview = extHostWebviews.createWebviewPanel({} as any, 'type', 'title', 1, {}); + }, undefined, new NullLogService(), NullApiDeprecationService); + + const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); + + const webview = extHostWebviewPanels.createWebviewPanel({} as any, 'type', 'title', 1, {}); function stripEndpointUuid(input: string) { return input.replace(/^https:\/\/[^\.]+?\./, ''); @@ -156,7 +159,7 @@ suite('ExtHostWebview', () => { function createNoopMainThreadWebviews() { - return new class extends mock() { + return new class extends mock() { $createWebviewPanel() { /* noop */ } $registerSerializer() { /* noop */ } $unregisterSerializer() { /* noop */ } diff --git a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts index d0f65701a26..b413ec9fa5d 100644 --- a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts @@ -10,7 +10,7 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { ExtHostDocumentsAndEditorsShape, ExtHostContext, ExtHostDocumentsShape, IWorkspaceTextEditDto } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostDocumentsAndEditorsShape, ExtHostContext, ExtHostDocumentsShape, IWorkspaceTextEditDto, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol'; import { mock } from 'vs/base/test/common/mock'; import { Event } from 'vs/base/common/event'; import { MainThreadTextEditors } from 'vs/workbench/api/browser/mainThreadEditors'; @@ -20,7 +20,7 @@ import { Position } from 'vs/editor/common/core/position'; import { IModelService } from 'vs/editor/common/services/modelService'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { TestFileService, TestEditorService, TestEditorGroupsService, TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { BulkEditService } from 'vs/workbench/services/bulkEdit/browser/bulkEditService'; +import { BulkEditService } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditService'; import { NullLogService, ILogService } from 'vs/platform/log/common/log'; import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { IReference, ImmortalReference } from 'vs/base/common/lifecycle'; @@ -170,6 +170,7 @@ suite('MainThreadEditors', () => { let model = modelService.createModel('something', null, resource); let workspaceResourceEdit: IWorkspaceTextEditDto = { + _type: WorkspaceEditType.Text, resource: resource, modelVersionId: model.getVersionId(), edit: { @@ -191,6 +192,7 @@ suite('MainThreadEditors', () => { let model = modelService.createModel('something', null, resource); let workspaceResourceEdit1: IWorkspaceTextEditDto = { + _type: WorkspaceEditType.Text, resource: resource, modelVersionId: model.getVersionId(), edit: { @@ -199,6 +201,7 @@ suite('MainThreadEditors', () => { } }; let workspaceResourceEdit2: IWorkspaceTextEditDto = { + _type: WorkspaceEditType.Text, resource: resource, modelVersionId: model.getVersionId(), edit: { @@ -221,9 +224,9 @@ suite('MainThreadEditors', () => { test(`applyWorkspaceEdit with only resource edit`, () => { return editors.$tryApplyWorkspaceEdit({ edits: [ - { oldUri: resource, newUri: resource, options: undefined }, - { oldUri: undefined, newUri: resource, options: undefined }, - { oldUri: resource, newUri: undefined, options: undefined } + { _type: WorkspaceEditType.File, oldUri: resource, newUri: resource, options: undefined }, + { _type: WorkspaceEditType.File, oldUri: undefined, newUri: resource, options: undefined }, + { _type: WorkspaceEditType.File, oldUri: resource, newUri: undefined, options: undefined } ] }).then((result) => { assert.equal(result, true); diff --git a/src/vs/workbench/test/browser/part.test.ts b/src/vs/workbench/test/browser/part.test.ts index a2bd94ea0ec..3fc67fcff6a 100644 --- a/src/vs/workbench/test/browser/part.test.ts +++ b/src/vs/workbench/test/browser/part.test.ts @@ -63,7 +63,7 @@ class MyPart2 extends SimplePart { const titleContainer = append(parent, $('div')); const titleLabel = append(titleContainer, $('span')); titleLabel.id = 'myPart.title'; - titleLabel.innerHTML = 'Title'; + titleLabel.innerText = 'Title'; return titleContainer; } @@ -72,7 +72,7 @@ class MyPart2 extends SimplePart { const contentContainer = append(parent, $('div')); const contentSpan = append(contentContainer, $('span')); contentSpan.id = 'myPart.content'; - contentSpan.innerHTML = 'Content'; + contentSpan.innerText = 'Content'; return contentContainer; } @@ -92,7 +92,7 @@ class MyPart3 extends SimplePart { const contentContainer = append(parent, $('div')); const contentSpan = append(contentContainer, $('span')); contentSpan.id = 'myPart.content'; - contentSpan.innerHTML = 'Content'; + contentSpan.innerText = 'Content'; return contentContainer; } diff --git a/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts b/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts index 1e618eb1943..5c399799eaf 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts @@ -464,8 +464,9 @@ suite('Workbench editor groups', () => { // Active && Pinned const input1 = input(); - const openedEditor = group.openEditor(input1, { active: true, pinned: true }); + const { editor: openedEditor, isNew } = group.openEditor(input1, { active: true, pinned: true }); assert.equal(openedEditor, input1); + assert.equal(isNew, true); assert.equal(group.count, 1); assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 1); @@ -575,11 +576,13 @@ suite('Workbench editor groups', () => { const input3 = input('3'); // Pinned and Active - let openedEditor = group.openEditor(input1, { pinned: true, active: true }); - assert.equal(openedEditor, input1); + let openedEditorResult = group.openEditor(input1, { pinned: true, active: true }); + assert.equal(openedEditorResult.editor, input1); + assert.equal(openedEditorResult.isNew, true); - openedEditor = group.openEditor(input1Copy, { pinned: true, active: true }); // opening copy of editor should still return existing one - assert.equal(openedEditor, input1); + openedEditorResult = group.openEditor(input1Copy, { pinned: true, active: true }); // opening copy of editor should still return existing one + assert.equal(openedEditorResult.editor, input1); + assert.equal(openedEditorResult.isNew, false); group.openEditor(input2, { pinned: true, active: true }); group.openEditor(input3, { pinned: true, active: true }); @@ -1145,7 +1148,7 @@ suite('Workbench editor groups', () => { // [] -> /index.html/ const indexHtml = input('index.html'); - let openedEditor = group.openEditor(indexHtml); + let openedEditor = group.openEditor(indexHtml).editor; assert.equal(openedEditor, indexHtml); assert.equal(group.activeEditor, indexHtml); assert.equal(group.previewEditor, indexHtml); @@ -1154,7 +1157,7 @@ suite('Workbench editor groups', () => { // /index.html/ -> /index.html/ const sameIndexHtml = input('index.html'); - openedEditor = group.openEditor(sameIndexHtml); + openedEditor = group.openEditor(sameIndexHtml).editor; assert.equal(openedEditor, indexHtml); assert.equal(group.activeEditor, indexHtml); assert.equal(group.previewEditor, indexHtml); @@ -1163,7 +1166,7 @@ suite('Workbench editor groups', () => { // /index.html/ -> /style.css/ const styleCss = input('style.css'); - openedEditor = group.openEditor(styleCss); + openedEditor = group.openEditor(styleCss).editor; assert.equal(openedEditor, styleCss); assert.equal(group.activeEditor, styleCss); assert.equal(group.previewEditor, styleCss); @@ -1172,7 +1175,7 @@ suite('Workbench editor groups', () => { // /style.css/ -> [/style.css/, test.js] const testJs = input('test.js'); - openedEditor = group.openEditor(testJs, { active: true, pinned: true }); + openedEditor = group.openEditor(testJs, { active: true, pinned: true }).editor; assert.equal(openedEditor, testJs); assert.equal(group.previewEditor, styleCss); assert.equal(group.activeEditor, testJs); diff --git a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts similarity index 97% rename from src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts rename to src/vs/workbench/test/browser/parts/editor/editorPane.test.ts index 82a0abe7d2a..15b7aba94ce 100644 --- a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { BaseEditor, EditorMemento } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane, EditorMemento } from 'vs/workbench/browser/parts/editor/editorPane'; import { EditorInput, EditorOptions, IEditorInputFactory, IEditorInputFactoryRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import * as Platform from 'vs/platform/registry/common/platform'; @@ -27,7 +27,7 @@ const NullThemeService = new TestThemeService(); let EditorRegistry: IEditorRegistry = Platform.Registry.as(Extensions.Editors); let EditorInputRegistry: IEditorInputFactoryRegistry = Platform.Registry.as(EditorExtensions.EditorInputFactories); -export class MyEditor extends BaseEditor { +export class MyEditor extends EditorPane { constructor(@ITelemetryService telemetryService: ITelemetryService) { super('MyEditor', NullTelemetryService, NullThemeService, new TestStorageService()); @@ -38,7 +38,7 @@ export class MyEditor extends BaseEditor { createEditor(): any { } } -export class MyOtherEditor extends BaseEditor { +export class MyOtherEditor extends EditorPane { constructor(@ITelemetryService telemetryService: ITelemetryService) { super('myOtherEditor', NullTelemetryService, NullThemeService, new TestStorageService()); @@ -96,9 +96,9 @@ class MyOtherInput extends EditorInput { } class MyResourceEditorInput extends ResourceEditorInput { } -suite('Workbench base editor', () => { +suite('Workbench EditorPane', () => { - test('BaseEditor API', async () => { + test('EditorPane API', async () => { let e = new MyEditor(NullTelemetryService); let input = new MyOtherInput(); let options = new EditorOptions(); @@ -106,7 +106,7 @@ suite('Workbench base editor', () => { assert(!e.isVisible()); assert(!e.input); - await e.setInput(input, options, CancellationToken.None); + await e.setInput(input, options, Object.create(null), CancellationToken.None); assert.strictEqual(input, e.input); const group = new TestEditorGroupView(1); e.setVisible(true, group); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 8840867e739..0ee2a6325ba 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -10,7 +10,7 @@ import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { IEditorInputWithOptions, IEditorIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInput, IEditorPane, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorInput, EditorOptions, EditorsOrder, IFileEditorInput, IEditorInputFactoryRegistry, IEditorInputFactory, Extensions as EditorExtensions, ISaveOptions, IMoveResult, ITextEditorPane, ITextDiffEditorPane, IVisibleEditorPane } from 'vs/workbench/common/editor'; +import { IEditorInputWithOptions, IEditorIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInput, IEditorPane, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorInput, EditorOptions, EditorsOrder, IFileEditorInput, IEditorInputFactoryRegistry, IEditorInputFactory, Extensions as EditorExtensions, ISaveOptions, IMoveResult, ITextEditorPane, ITextDiffEditorPane, IVisibleEditorPane, IEditorOpenContext } from 'vs/workbench/common/editor'; import { IEditorOpeningEvent, EditorServiceImpl, IEditorGroupView, IEditorGroupsAccessor } from 'vs/workbench/browser/parts/editor/editor'; import { Event, Emitter } from 'vs/base/common/event'; import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup'; @@ -91,7 +91,7 @@ import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { Registry } from 'vs/platform/registry/common/platform'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { CancellationToken } from 'vs/base/common/cancellation'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; @@ -1068,12 +1068,12 @@ export class TestEditorInput extends EditorInput { } export function registerTestEditor(id: string, inputs: SyncDescriptor[], factoryInputId?: string): IDisposable { - class TestEditorControl extends BaseEditor { + class TestEditor extends EditorPane { constructor() { super(id, NullTelemetryService, new TestThemeService(), new TestStorageService()); } - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - super.setInput(input, options, token); + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + super.setInput(input, options, context, token); await input.resolve(); } @@ -1085,7 +1085,7 @@ export function registerTestEditor(id: string, inputs: SyncDescriptor(Extensions.Editors).registerEditor(EditorDescriptor.create(TestEditorControl, id, 'Test Editor Control'), inputs)); + disposables.add(Registry.as(Extensions.Editors).registerEditor(EditorDescriptor.create(TestEditor, id, 'Test Editor Control'), inputs)); if (factoryInputId) { diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 9e555a89207..e2211240cd0 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -215,7 +215,6 @@ export class TestElectronService implements IElectronService { async exit(code: number): Promise { } async openDevTools(options?: Electron.OpenDevToolsOptions | undefined): Promise { } async toggleDevTools(): Promise { } - async startCrashReporter(options: Electron.CrashReporterStartOptions): Promise { } async resolveProxy(url: string): Promise { return undefined; } async readClipboardText(type?: 'selection' | 'clipboard' | undefined): Promise { return ''; } async writeClipboardText(text: string, type?: 'selection' | 'clipboard' | undefined): Promise { } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 6d6d66e1381..6b38f0bcc3c 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -55,7 +55,6 @@ import 'vs/workbench/browser/parts/views/viewsService'; import 'vs/platform/undoRedo/common/undoRedoService'; import 'vs/workbench/services/uriIdentity/common/uriIdentityService'; import 'vs/workbench/services/extensions/browser/extensionUrlHandler'; -import 'vs/workbench/services/bulkEdit/browser/bulkEditService'; import 'vs/workbench/services/keybinding/common/keybindingEditing'; import 'vs/workbench/services/decorations/browser/decorationsService'; import 'vs/workbench/services/progress/browser/progressService'; @@ -165,7 +164,8 @@ import 'vs/workbench/contrib/files/browser/files.contribution'; import 'vs/workbench/contrib/backup/common/backup.contribution'; // bulkEdit -import 'vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution'; +import 'vs/workbench/contrib/bulkEdit/browser/bulkEditService'; +import 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution'; // Search import 'vs/workbench/contrib/search/browser/search.contribution'; @@ -199,6 +199,7 @@ import 'vs/workbench/contrib/url/browser/url.contribution'; // Webview import 'vs/workbench/contrib/webview/browser/webview.contribution'; +import 'vs/workbench/contrib/webviewView/browser/webviewView.contribution'; import 'vs/workbench/contrib/customEditor/browser/customEditor.contribution'; // Extensions Management diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index d52fd0e9c7a..c9eec9b1ae1 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -41,7 +41,6 @@ import 'vs/workbench/services/output/electron-browser/outputChannelModelService' import 'vs/workbench/services/textfile/electron-browser/nativeTextFileService'; import 'vs/workbench/services/dialogs/electron-browser/dialogService'; import 'vs/workbench/services/keybinding/electron-browser/nativeKeymapService'; -import 'vs/workbench/services/keybinding/electron-browser/keybinding.contribution'; import 'vs/workbench/services/extensions/electron-browser/extensionService'; import 'vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService'; import 'vs/workbench/services/extensionManagement/electron-browser/extensionTipsService'; @@ -55,6 +54,8 @@ import 'vs/workbench/services/workspaces/electron-browser/workspaceEditingServic import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncMachinesService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncAccountService'; +import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreManagementService'; +import 'vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService'; import 'vs/workbench/services/sharedProcess/electron-browser/sharedProcessService'; import 'vs/workbench/services/localizations/electron-browser/localizationsService'; import 'vs/workbench/services/path/electron-browser/pathService'; @@ -63,18 +64,16 @@ import 'vs/workbench/services/experiment/electron-browser/experimentService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { KeytarCredentialsService } from 'vs/platform/credentials/node/credentialsService'; -import { IUserDataAutoSyncService, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; -import { UserDataAutoSyncService } from 'vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { TunnelService } from 'vs/platform/remote/node/tunnelService'; import { ITimerService } from 'vs/workbench/services/timer/browser/timerService'; import { TimerService } from 'vs/workbench/services/timer/electron-browser/timerService'; +import { IUserDataInitializationService, UserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; registerSingleton(ICredentialsService, KeytarCredentialsService, true); -registerSingleton(IUserDataSyncStoreManagementService, UserDataSyncStoreManagementService); -registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); registerSingleton(ITunnelService, TunnelService); registerSingleton(ITimerService, TimerService); +registerSingleton(IUserDataInitializationService, UserDataInitializationService); //#endregion @@ -135,6 +134,5 @@ import 'vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribu // Configuration Exporter import 'vs/workbench/contrib/configExporter/electron-browser/configurationExportHelper.contribution'; -import { UserDataSyncStoreManagementService } from 'vs/workbench/contrib/userDataSync/electron-browser/userDataSyncStoreManagementService'; //#endregion diff --git a/src/vs/workbench/workbench.desktop.sandbox.main.ts b/src/vs/workbench/workbench.desktop.sandbox.main.ts new file mode 100644 index 00000000000..1d2f2a4150d --- /dev/null +++ b/src/vs/workbench/workbench.desktop.sandbox.main.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. + *--------------------------------------------------------------------------------------------*/ + + +// ####################################################################### +// ### ### +// ### !!! PLEASE ADD COMMON IMPORTS INTO WORKBENCH.COMMON.MAIN.TS !!! ### +// ### ### +// ####################################################################### + + +//#region --- workbench common & sandbox + +import 'vs/workbench/workbench.sandbox.main'; + +//#endregion + + +//#region --- workbench actions + + +//#endregion + + +//#region --- workbench (desktop main) + +import 'vs/workbench/electron-sandbox/desktop.main'; + +//#endregion + + +//#region --- workbench services + + +//#endregion + + +//#region --- workbench contributions + + +//#endregion diff --git a/src/vs/workbench/workbench.sandbox.main.ts b/src/vs/workbench/workbench.sandbox.main.ts index 4ab428b3565..038fcb6f660 100644 --- a/src/vs/workbench/workbench.sandbox.main.ts +++ b/src/vs/workbench/workbench.sandbox.main.ts @@ -28,7 +28,7 @@ import 'vs/workbench/services/update/electron-sandbox/updateService'; import 'vs/workbench/services/url/electron-sandbox/urlService'; import 'vs/workbench/services/lifecycle/electron-sandbox/lifecycleService'; import 'vs/workbench/services/title/electron-sandbox/titleService'; -import 'vs/workbench/services/host/electron-sandbox/desktopHostService'; +import 'vs/workbench/services/host/electron-sandbox/nativeHostService'; import 'vs/workbench/services/request/electron-sandbox/requestService'; import 'vs/workbench/services/extensionResourceLoader/electron-sandbox/extensionResourceLoaderService'; import 'vs/workbench/services/clipboard/electron-sandbox/clipboardService'; diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index ce168ccdd0a..4386dfc2857 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -8,7 +8,6 @@ import { main } from 'vs/workbench/browser/web.main'; import { UriComponents, URI } from 'vs/base/common/uri'; import { IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, FileChangeType } from 'vs/platform/files/common/files'; import { IWebSocketFactory, IWebSocket } from 'vs/platform/remote/browser/browserSocketFactory'; -import { ICredentialsProvider } from 'vs/workbench/services/credentials/browser/credentialsService'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { IURLCallbackProvider } from 'vs/workbench/services/url/browser/urlService'; import { LogLevel } from 'vs/platform/log/common/log'; @@ -19,6 +18,7 @@ import { IWorkspaceProvider, IWorkspace } from 'vs/workbench/services/host/brows import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IProductConfiguration } from 'vs/platform/product/common/productService'; import { mark } from 'vs/base/common/performance'; +import { ICredentialsProvider } from 'vs/platform/credentials/common/credentials'; interface IResourceUriProvider { (uri: URI): URI; @@ -121,6 +121,41 @@ interface IHomeIndicator { title: string; } +interface IWindowIndicator { + + /** + * Triggering this event will cause the window indicator to update. + */ + onDidChange: Event; + + /** + * Label of the window indicator may include octicons + * e.g. `$(remote) label` + */ + label: string; + + /** + * Tooltip of the window indicator should not include + * octicons and be descriptive. + */ + tooltip: string; + + /** + * If provided, overrides the default command that + * is executed when clicking on the window indicator. + */ + command?: string; +} + +interface IInitialColorTheme { + themeType: 'light' | 'dark' | 'hc'; + + /** + * a list of workbench colors + */ + colors?: { [colorId: string]: string }; +} + interface IDefaultSideBarLayout { visible?: boolean; containers?: ({ @@ -204,6 +239,11 @@ interface IWorkbenchConstructionOptions { */ readonly connectionToken?: string; + /** + * Session id of the current authenticated user + */ + readonly authenticationSessionId?: string; + /** * An endpoint to serve iframe content ("webview") from. This is required * to provide full security isolation from the workbench host. @@ -231,6 +271,11 @@ interface IWorkbenchConstructionOptions { */ readonly tunnelProvider?: ITunnelProvider; + /** + * Endpoints to be used for proxying authentication code exchange calls in the browser. + */ + readonly codeExchangeProxyEndpoints?: { [providerId: string]: string } + //#endregion @@ -247,11 +292,6 @@ interface IWorkbenchConstructionOptions { */ userDataProvider?: IFileSystemProvider; - /** - * Session id of the current authenticated user - */ - readonly authenticationSessionId?: string; - /** * Enables user data sync by default and syncs into the current authenticated user account using the provided [authenticationSessionId}(#authenticationSessionId). */ @@ -345,6 +385,20 @@ interface IWorkbenchConstructionOptions { */ readonly productConfiguration?: Partial; + /** + * Optional override for properties of the window indicator in the status bar. + */ + readonly windowIndicator?: IWindowIndicator; + + /** + * Specifies the default theme type (LIGHT, DARK..) and allows to provide initial colors that are shown + * until the color theme that is specified in the settings (`editor.colorTheme`) is loaded and applied. + * Once there are persisted colors from a last run these will be used. + * + * The idea is that the colors match the main colors from the theme defined in the `configurationDefaults`. + */ + readonly initialColorTheme?: IInitialColorTheme; + //#endregion @@ -360,11 +414,6 @@ interface IWorkbenchConstructionOptions { */ readonly driver?: boolean; - /** - * Endpoints to be used for proxying authentication code exchange calls in the browser. - */ - readonly codeExchangeProxyEndpoints?: { [providerId: string]: string } - //#endregion } @@ -504,6 +553,7 @@ export { // Branding IHomeIndicator, IProductConfiguration, + IWindowIndicator, // Default layout IDefaultView, diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 0669178db4c..f02bbbf874b 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -46,7 +46,6 @@ import 'vs/workbench/services/workspaces/browser/workspaceEditingService'; import 'vs/workbench/services/dialogs/browser/dialogService'; import 'vs/workbench/services/dialogs/browser/fileDialogService'; import 'vs/workbench/services/host/browser/browserHostService'; -import 'vs/workbench/services/request/browser/requestService'; import 'vs/workbench/services/lifecycle/browser/lifecycleService'; import 'vs/workbench/services/clipboard/browser/clipboardService'; import 'vs/workbench/services/extensionResourceLoader/browser/extensionResourceLoaderService'; @@ -72,7 +71,7 @@ import { UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { IUserDataSyncAccountService, UserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; -import { UserDataAutoSyncService } from 'vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService'; +import { UserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; import { AccessibilityService } from 'vs/platform/accessibility/common/accessibilityService'; import { ITitleService } from 'vs/workbench/services/title/common/titleService'; import { TitlebarPart } from 'vs/workbench/browser/parts/titlebar/titlebarPart'; @@ -91,8 +90,8 @@ registerSingleton(IUserDataSyncMachinesService, UserDataSyncMachinesService); registerSingleton(IUserDataSyncBackupStoreService, UserDataSyncBackupStoreService); registerSingleton(IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService); registerSingleton(IUserDataSyncAccountService, UserDataSyncAccountService); -registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); -registerSingleton(IUserDataSyncService, UserDataSyncService); +registerSingleton(IUserDataSyncService, UserDataSyncService, true); +registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService, true); registerSingleton(ITitleService, TitlebarPart); registerSingleton(IExtensionTipsService, ExtensionTipsService); registerSingleton(ITimerService, TimerService); @@ -131,4 +130,7 @@ import 'vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut.con // Issues import 'vs/workbench/contrib/issue/browser/issue.web.contribution'; +// Extensions Management (// TODO@sandbox TODO@ben move back into common/extensions.contribution.ts when 'semver-umd' can be loaded) +import 'vs/workbench/contrib/extensions/browser/extensions.web.contribution'; + //#endregion diff --git a/test/integration/browser/src/index.ts b/test/integration/browser/src/index.ts index 91bdd3a8322..5ce67dd0548 100644 --- a/test/integration/browser/src/index.ts +++ b/test/integration/browser/src/index.ts @@ -29,7 +29,9 @@ if (optimist.argv.help) { const width = 1200; const height = 800; -async function runTestsInBrowser(browserType: 'chromium' | 'firefox' | 'webkit', endpoint: url.UrlWithStringQuery, server: cp.ChildProcess): Promise { +type BrowserType = 'chromium' | 'firefox' | 'webkit'; + +async function runTestsInBrowser(browserType: BrowserType, endpoint: url.UrlWithStringQuery, server: cp.ChildProcess): Promise { const args = process.platform === 'linux' && browserType === 'chromium' ? ['--no-sandbox'] : undefined; // disable sandbox to run chrome on certain Linux distros const browser = await playwright[browserType].launch({ headless: !Boolean(optimist.argv.debug), args }); const context = await browser.newContext(); @@ -78,7 +80,7 @@ function pkill(pid: number): Promise { }); } -async function launchServer(): Promise<{ endpoint: url.UrlWithStringQuery, server: cp.ChildProcess }> { +async function launchServer(browserType: BrowserType): Promise<{ endpoint: url.UrlWithStringQuery, server: cp.ChildProcess }> { // Ensure a tmp user-data-dir is used for the tests const tmpDir = tmp.dirSync({ prefix: 't' }); @@ -89,6 +91,7 @@ async function launchServer(): Promise<{ endpoint: url.UrlWithStringQuery, serve const env = { VSCODE_AGENT_FOLDER: userDataDir, + VSCODE_BROWSER: browserType, ...process.env }; @@ -130,7 +133,7 @@ async function launchServer(): Promise<{ endpoint: url.UrlWithStringQuery, serve }); } -launchServer().then(async ({ endpoint, server }) => { +launchServer(optimist.argv.browser).then(async ({ endpoint, server }) => { return runTestsInBrowser(optimist.argv.browser, endpoint, server); }, error => { console.error(error); diff --git a/test/smoke/test/index.js b/test/smoke/test/index.js index 5e33b701fa3..a2d16f5e306 100644 --- a/test/smoke/test/index.js +++ b/test/smoke/test/index.js @@ -7,13 +7,14 @@ const path = require('path'); const Mocha = require('mocha'); const minimist = require('minimist'); -const suite = 'Smoke Tests'; - const [, , ...args] = process.argv; const opts = minimist(args, { + boolean: 'web', string: ['f', 'g'] }); +const suite = opts['web'] ? 'Browser Smoke Tests' : 'Smoke Tests'; + const options = { color: true, timeout: 60000, @@ -27,7 +28,7 @@ if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/test/unit/browser/index.js b/test/unit/browser/index.js index 926d96f6352..ba30a81e7b5 100644 --- a/test/unit/browser/index.js +++ b/test/unit/browser/index.js @@ -10,6 +10,7 @@ const glob = require('glob'); const fs = require('fs'); const events = require('events'); const mocha = require('mocha'); +const MochaJUnitReporter = require('mocha-junit-reporter'); const url = require('url'); const minimatch = require('minimatch'); const playwright = require('playwright'); @@ -37,30 +38,44 @@ if (argv.help) { } const withReporter = (function () { - const reporterPath = path.join(path.dirname(require.resolve('mocha')), 'lib', 'reporters', argv.reporter); - let ctor; - - try { - ctor = require(reporterPath); - } catch (err) { - try { - ctor = require(argv.reporter); - } catch (err) { - ctor = process.platform === 'win32' ? mocha.reporters.List : mocha.reporters.Spec; - console.warn(`could not load reporter: ${argv.reporter}, using ${ctor.name}`); + if (argv.tfs) { + { + return (browserType, runner) => { + new mocha.reporters.Spec(runner); + new MochaJUnitReporter(runner, { + reporterOptions: { + testsuitesTitle: `${argv.tfs} ${process.platform}`, + mochaFile: process.env.BUILD_ARTIFACTSTAGINGDIRECTORY ? path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${browserType}-${argv.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined + } + }); + } } + } else { + const reporterPath = path.join(path.dirname(require.resolve('mocha')), 'lib', 'reporters', argv.reporter); + let ctor; + + try { + ctor = require(reporterPath); + } catch (err) { + try { + ctor = require(argv.reporter); + } catch (err) { + ctor = process.platform === 'win32' ? mocha.reporters.List : mocha.reporters.Spec; + console.warn(`could not load reporter: ${argv.reporter}, using ${ctor.name}`); + } + } + + function parseReporterOption(value) { + let r = /^([^=]+)=(.*)$/.exec(value); + return r ? { [r[1]]: r[2] } : {}; + } + + let reporterOptions = argv['reporter-options']; + reporterOptions = typeof reporterOptions === 'string' ? [reporterOptions] : reporterOptions; + reporterOptions = reporterOptions.reduce((r, o) => Object.assign(r, parseReporterOption(o)), {}); + + return (_, runner) => new ctor(runner, { reporterOptions }) } - - function parseReporterOption(value) { - let r = /^([^=]+)=(.*)$/.exec(value); - return r ? { [r[1]]: r[2] } : {}; - } - - let reporterOptions = argv['reporter-options']; - reporterOptions = typeof reporterOptions === 'string' ? [reporterOptions] : reporterOptions; - reporterOptions = reporterOptions.reduce((r, o) => Object.assign(r, parseReporterOption(o)), {}); - - return (runner) => new ctor(runner, { reporterOptions }) })() const outdir = argv.build ? 'out-build' : 'out'; @@ -137,7 +152,7 @@ async function runTestsInBrowser(testModules, browserType) { console[msg.type()](msg.text(), await Promise.all(msg.args().map(async arg => await arg.jsonValue()))); }); - withReporter(new EchoRunner(emitter, browserType.toUpperCase())); + withReporter(browserType, new EchoRunner(emitter, browserType.toUpperCase())); // collection failures for console printing const fails = []; diff --git a/test/unit/electron/index.js b/test/unit/electron/index.js index dcd00923c9b..6685ff8f713 100644 --- a/test/unit/electron/index.js +++ b/test/unit/electron/index.js @@ -9,9 +9,13 @@ const { join } = require('path'); const path = require('path'); const mocha = require('mocha'); const events = require('events'); -// const MochaJUnitReporter = require('mocha-junit-reporter'); +const MochaJUnitReporter = require('mocha-junit-reporter'); const url = require('url'); +// Disable render process reuse, we still have +// non-context aware native modules in the renderer. +app.allowRendererProcessReuse = false; + const defaultReporterName = process.platform === 'win32' ? 'list' : 'spec'; const optimist = require('optimist') @@ -136,13 +140,12 @@ app.on('ready', () => { if (argv.tfs) { new mocha.reporters.Spec(runner); - // TODO@deepak the mocha Junit reporter seems to cause a hang when running with Electron 6 inside docker container - // new MochaJUnitReporter(runner, { - // reporterOptions: { - // testsuitesTitle: `${argv.tfs} ${process.platform}`, - // mochaFile: process.env.BUILD_ARTIFACTSTAGINGDIRECTORY ? path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${argv.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined - // } - // }); + new MochaJUnitReporter(runner, { + reporterOptions: { + testsuitesTitle: `${argv.tfs} ${process.platform}`, + mochaFile: process.env.BUILD_ARTIFACTSTAGINGDIRECTORY ? path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${argv.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined + } + }); } else { const reporterPath = path.join(path.dirname(require.resolve('mocha')), 'lib', 'reporters', argv.reporter); let Reporter; diff --git a/yarn.lock b/yarn.lock index b2fbf543af3..07c68d7b74f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2744,10 +2744,10 @@ electron-to-chromium@^1.2.7: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.27.tgz#78ecb8a399066187bb374eede35d9c70565a803d" integrity sha1-eOy4o5kGYYe7N07t412ccFZagD0= -electron@7.3.2: - version "7.3.2" - resolved "https://registry.yarnpkg.com/electron/-/electron-7.3.2.tgz#184b69fe9089693e179b3b34effa975dfc8e505d" - integrity sha512-5uSWVfCJogiPiU0G+RKi4ECnNs0gPNjAwYVE9KR7RXaOJYcpNIC5RFejaaUnuRoBssJ5B1n/5WU6wDUxvPajWQ== +electron@9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/electron/-/electron-9.2.1.tgz#54ef574e1af4ae967b5efa94312f1b6458d44a02" + integrity sha512-ZsetaQjXB8+9/EFW1FnfK4ukpkwXCxMEaiKiUZhZ0ZLFlLnFCpe0Bg4vdDf7e4boWGcnlgN1jAJpBw7w0eXuqA== dependencies: "@electron/get" "^1.0.1" "@types/node" "^12.0.12" @@ -6237,10 +6237,10 @@ native-is-elevated@0.4.1: resolved "https://registry.yarnpkg.com/native-is-elevated/-/native-is-elevated-0.4.1.tgz#f6391aafb13441f5b949b39ae0b466b06e7f3986" integrity sha512-2vBXCXCXYKLDjP0WzrXs/AFjDb2njPR31EbGiZ1mR2fMJg211xClK1Xm19RXve35kvAL4dBKOFGCMIyc2+pPsw== -native-keymap@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/native-keymap/-/native-keymap-2.1.2.tgz#9773313f619d4c2b66b452cf036310a145523b59" - integrity sha512-n+oe+sxaauCFxomkl9Xrw1iUp88jTamMaGJSHNSGZ8rkIN9N+Wi6KIvBO8x3nmFxLI27KWu1d8IrLBxFKPNQag== +native-keymap@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/native-keymap/-/native-keymap-2.2.0.tgz#940aeb4ae05776dd44dbd9a80dba5342fd49fb8c" + integrity sha512-rWT9mf5f4vMGluXoIoxKSZy76fcVgMvk5jC4meBaOP2GfMJAI7Obtdzpa1Fa1qZCBtZa+OAYV8vlc8dKPOhUNw== native-watchdog@1.3.0: version "1.3.0" @@ -9369,10 +9369,10 @@ typescript@^2.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" integrity sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q= -typescript@^4.0.0-dev.20200803: - version "4.0.0-dev.20200803" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.0-dev.20200803.tgz#ea8b0e9fb2ee3085598ff200c8568f04f4cbb2ba" - integrity sha512-f/jDkFqCs0gbUd5MCUijO9u3AOMx1x1HdRDDHSidlc6uPVEkRduxjeTFhIXbGutO7ivzv+aC2sxH+1FQwsyBcg== +typescript@^4.1.0-dev.20200824: + version "4.1.0-dev.20200824" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.0-dev.20200824.tgz#34c92d9b6e5124600658c0d4e9b8c125beaf577d" + integrity sha512-hTJfocmebnMKoqRw/xs3bL61z87XXtvOUwYtM7zaCX9mAvnfdo1x1bzQlLZAsvdzRIgAHPJQYbqYHKygWkDw6g== uc.micro@^1.0.1, uc.micro@^1.0.3: version "1.0.3"