diff --git a/.eslintrc.json b/.eslintrc.json index 0abb18adee2..8761c1c1813 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -725,6 +725,18 @@ "*" // node modules ] }, + { + "target": "**/vs/code/browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,browser}/**", + "**/vs/base/parts/**/{common,browser}/**", + "**/vs/platform/**/{common,browser}/**", + "**/vs/code/**/{common,browser}/**", + "**/vs/workbench/workbench.web.api" + ] + }, { "target": "**/vs/code/node/**", "restrictions": [ @@ -795,6 +807,18 @@ "**/vs/workbench/workbench.common.main" ] }, + { + "target": "**/src/vs/workbench/workbench.web.api.ts", + "restrictions": [ + "vs/nls", + "**/vs/base/**/{common,browser}/**", + "**/vs/base/parts/**/{common,browser}/**", + "**/vs/platform/**/{common,browser}/**", + "**/vs/editor/**", + "**/vs/workbench/**/{common,browser}/**", + "**/vs/workbench/workbench.web.main" + ] + }, { "target": "**/src/vs/workbench/workbench.sandbox.main.ts", "restrictions": [ diff --git a/.github/classifier.json b/.github/classifier.json index 8b5b7941d05..420cce5bfaf 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -1,6 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/vscode-github-triage-actions/master/classifier-deep/apply/apply-labels/deep-classifier-config.schema.json", "assignees": { + "joaomoreno": {"accuracy": 1.5}, "JacksonKearl": {"accuracy": 0.5} }, "labels": { @@ -71,10 +72,10 @@ "file-watcher": {"assign": ["bpasero"]}, "font-rendering": {"assign": []}, "formatting": {"assign": []}, - "git": {"assign": ["joaomoreno"]}, + "git": {"assign": ["joaomoreno"], "accuracy": 1.5}, "gpu": {"assign": ["deepak1556"]}, "grammar": {"assign": ["mjbvz"]}, - "grid-view": {"assign": ["joaomoreno"]}, + "grid-view": {"assign": ["joaomoreno"], "accuracy": 1.5}, "html": {"assign": ["aeschli"]}, "i18n": {"assign": []}, "icon-brand": {"assign": []}, @@ -85,7 +86,7 @@ "integrated-terminal-links": {"assign": ["Tyriar"]}, "integration-test": {"assign": []}, "intellisense-config": {"assign": []}, - "ipc": {"assign": ["joaomoreno"]}, + "ipc": {"assign": ["joaomoreno"], "accuracy": 1.5}, "issue-bot": {"assign": ["chrmarti"]}, "issue-reporter": {"assign": ["RMacfarlane"]}, "javascript": {"assign": ["mjbvz"]}, @@ -97,7 +98,7 @@ "languages-diagnostics": {"assign": ["jrieken"]}, "layout": {"assign": ["sbatten"]}, "lcd-text-rendering": {"assign": []}, - "list": {"assign": ["joaomoreno"]}, + "list": {"assign": ["joaomoreno"], "accuracy": 1.5}, "log": {"assign": []}, "markdown": {"assign": ["mjbvz"]}, "marketplace": {"assign": []}, @@ -110,7 +111,7 @@ "perf-bloat": {"assign": []}, "perf-startup": {"assign": []}, "php": {"assign": ["roblourens"]}, - "portable-mode": {"assign": ["joaomoreno"]}, + "portable-mode": {"assign": ["joaomoreno"], "accuracy": 1.5}, "proxy": {"assign": []}, "quick-pick": {"assign": ["chrmarti"]}, "references-viewlet": {"assign": ["jrieken"]}, @@ -118,8 +119,8 @@ "remote": {"assign": []}, "remote-explorer": {"assign": ["alexr00"]}, "rename": {"assign": ["jrieken"]}, - "scm": {"assign": ["joaomoreno"]}, - "screencast-mode": {"assign": ["joaomoreno"]}, + "scm": {"assign": ["joaomoreno"], "accuracy": 1.5}, + "screencast-mode": {"assign": ["lszomoru"]}, "search": {"assign": ["roblourens"]}, "search-editor": {"assign": ["JacksonKearl"]}, "search-replace": {"assign": ["sandy081"]}, @@ -129,9 +130,9 @@ "simple-file-dialog": {"assign": ["alexr00"]}, "smart-select": {"assign": ["jrieken"]}, "smoke-test": {"assign": []}, - "snap": {"assign": ["joaomoreno"]}, + "snap": {"assign": ["joaomoreno"], "accuracy": 1.5}, "snippets": {"assign": ["jrieken"]}, - "splitview": {"assign": ["joaomoreno"]}, + "splitview": {"assign": ["joaomoreno"], "accuracy": 1.5}, "suggest": {"assign": ["jrieken"]}, "tasks": {"assign": ["alexr00"]}, "telemetry": {"assign": []}, @@ -140,7 +141,7 @@ "timeline-git": {"assign": ["eamodio"]}, "titlebar": {"assign": ["sbatten"]}, "tokenization": {"assign": []}, - "tree": {"assign": ["joaomoreno"]}, + "tree": {"assign": ["joaomoreno"], "accuracy": 1.5}, "typescript": {"assign": ["mjbvz"]}, "undo-redo": {"assign": []}, "unit-test": {"assign": []}, diff --git a/.github/commands.json b/.github/commands.json index 45e8b89deae..1b2bc516842 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -297,7 +297,7 @@ ], "action": "close", "addLabel": "*caused-by-extension", - "comment": "It looks like this is caused by the Powershell extension. Please file it with the repository [here](https://github.com/PowerShell/vscode-powershell). Make sure to check their issue reporting template and provide them relevant information such as the extension version you're using. See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines for more information.\n\nHappy Coding!" + "comment": "It looks like this is caused by the PowerShell extension. Please file it with the repository [here](https://github.com/PowerShell/vscode-powershell). Make sure to check their issue reporting template and provide them relevant information such as the extension version you're using. See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines for more information.\n\nHappy Coding!" }, { "type": "comment", @@ -351,6 +351,18 @@ "addLabel": "*caused-by-extension", "comment": "It looks like this is caused by the Java Debugger extension. Please file it with the repository [here](https://github.com/Microsoft/vscode-java-debug). Make sure to check their issue reporting template and provide them relevant information such as the extension version you're using. See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines for more information.\n\nHappy Coding!" }, + { + "type": "comment", + "name": "gifPlease", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "comment", + "comment": "Thanks for reporting this issue! Unfortunately, it's hard for us to understand what issue you're seeing. Please help us out by providing a screen recording showing exactly what isn't working as expected. While we can work with most standard formats, `.gif` files are preferred as they are displayed inline on GitHub. You may find https://gifcap.dev helpful as a browser-based gif recording tool.\n\nIf the issue depends on keyboard input, you can help us by enabling screencast mode for the recording (`Developer: Toggle Screencast Mode` in the command palette).\n\nHappy coding!" + }, { "type": "comment", "name": "label", diff --git a/.github/workflows/feature-request.yml b/.github/workflows/feature-request.yml index 893bba74a16..cdd65c77202 100644 --- a/.github/workflows/feature-request.yml +++ b/.github/workflows/feature-request.yml @@ -34,8 +34,8 @@ jobs: featureRequestLabel: feature-request upvotesRequired: 20 numCommentsOverride: 20 - initComment: "This feature request is now a candidate for our backlog. The community has 60 days to upvote the issue. If it receives 20 upvotes we will move it to our backlog. If not, we will close it. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" - warnComment: "This feature request has not yet received the 20 community upvotes it takes to make to our backlog. 10 days to go. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding" + initComment: "This feature request is now a candidate for our backlog. The community has 60 days to [upvote](https://github.com/microsoft/vscode/wiki/Issues-Triaging#up-voting-a-feature-request) the issue. If it receives 20 upvotes we will move it to our backlog. If not, we will close it. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" + warnComment: "This feature request has not yet received the 20 community [upvotes](https://github.com/microsoft/vscode/wiki/Issues-Triaging#up-voting-a-feature-request) it takes to make to our backlog. 10 days to go. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" acceptComment: ":slightly_smiling_face: This feature request received a sufficient number of community upvotes and we moved it to our backlog. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" rejectComment: ":slightly_frowning_face: In the last 60 days, this feature request has received less than 20 community upvotes and we closed it. Still a big Thank You to you for taking the time to create this issue! To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" warnDays: 10 diff --git a/.github/workflows/rich-navigation.yml b/.github/workflows/rich-navigation.yml index d1ac9f09921..185c770dd0f 100644 --- a/.github/workflows/rich-navigation.yml +++ b/.github/workflows/rich-navigation.yml @@ -16,7 +16,11 @@ jobs: run: yarn --frozen-lockfile env: CHILD_CONCURRENCY: 1 - - uses: microsoft/RichCodeNavIndexer@master + - 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 repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index e73dd4d9e8c..0fe46b6eadc 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ out-vscode-reh-web-min/ out-vscode-reh-web-pkg/ out-vscode-web/ out-vscode-web-min/ +out-vscode-web-pkg/ src/vs/server resources/server build/node_modules diff --git a/.vscode/launch.json b/.vscode/launch.json index 14bceb63786..577b733df80 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -176,6 +176,24 @@ "order": 6 } }, + { + "type": "extensionHost", + "request": "launch", + "name": "VS Code Custom Editor Tests", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}/extensions/vscode-custom-editor-tests/test-workspace", + "--extensionDevelopmentPath=${workspaceFolder}/extensions/vscode-custom-editor-tests", + "--extensionTestsPath=${workspaceFolder}/extensions/vscode-custom-editor-tests/out/test" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "presentation": { + "group": "5_tests", + "order": 6 + } + }, { "type": "chrome", "request": "attach", @@ -198,10 +216,10 @@ "port": 9222, "timeout": 20000, "env": { - "VSCODE_EXTHOST_WILL_SEND_SOCKET": null + "VSCODE_EXTHOST_WILL_SEND_SOCKET": null, + "VSCODE_SKIP_PRELAUNCH": "1" }, "cleanUp": "wholeBrowser", - "breakOnLoad": false, "urlFilter": "*workbench.html*", "runtimeArgs": [ "--inspect=5875", @@ -214,7 +232,8 @@ "outFiles": [ "${workspaceFolder}/out/**/*.js" ], - "browserLaunchLocation": "workspace" + "browserLaunchLocation": "workspace", + "preLaunchTask": "Ensure Prelaunch Dependencies", }, { "type": "node", diff --git a/.vscode/notebooks/inbox.github-issues b/.vscode/notebooks/inbox.github-issues index 8931e7b0b3b..979da52c04d 100644 --- a/.vscode/notebooks/inbox.github-issues +++ b/.vscode/notebooks/inbox.github-issues @@ -8,8 +8,17 @@ { "kind": 2, "language": "github-issues", - "value": "$inbox=repo:microsoft/vscode is:open no:assignee -label:feature-request -label:testplan-item -label:plan-item ", - "editable": true + "value": "$inbox=repo:microsoft/vscode is:open no:assignee -label:feature-request -label:testplan-item -label:plan-item " + }, + { + "kind": 1, + "language": "markdown", + "value": "## Inbox tracking and Issue triage" + }, + { + "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." }, { "kind": 1, @@ -32,7 +41,7 @@ { "kind": 2, "language": "github-issues", - "value": "$inbox", - "editable": false + "value": "$inbox -label:emmet", + "editable": true } ] \ No newline at end of file diff --git a/.vscode/notebooks/verification.github-issues b/.vscode/notebooks/verification.github-issues index 6f32df8e077..79f7f8a5c0e 100644 --- a/.vscode/notebooks/verification.github-issues +++ b/.vscode/notebooks/verification.github-issues @@ -14,7 +14,7 @@ { "kind": 2, "language": "github-issues", - "value": "$repos=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks \n$milestone=milestone:\"June 2020\"", + "value": "$repos=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks \n$milestone=milestone:\"July 2020\"", "editable": true }, { @@ -44,7 +44,8 @@ { "kind": 1, "language": "markdown", - "value": "### All" + "value": "### All", + "editable": true }, { "kind": 2, 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/.vscode/tasks.json b/.vscode/tasks.json index 213183bc072..f2857f8a280 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -186,6 +186,14 @@ "source": "eslint", "base": "$eslint-stylish" } - } + }, + { + "type": "shell", + "command": "node build/lib/preLaunch.js", + "label": "Ensure Prelaunch Dependencies", + "presentation": { + "reveal": "silent" + } + }, ] } diff --git a/.yarnrc b/.yarnrc index 4c5125d8923..135e10442a7 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,3 @@ disturl "https://atom.io/download/electron" -target "8.3.3" +target "7.3.2" runtime "electron" diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 0388430595d..ac17c9eaa64 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -61,9 +61,9 @@ This project incorporates components from the projects listed below. The origina 54. textmate/yaml.tmbundle (https://github.com/textmate/yaml.tmbundle) 55. TypeScript-TmLanguage version 0.1.8 (https://github.com/Microsoft/TypeScript-TmLanguage) 56. TypeScript-TmLanguage version 1.0.0 (https://github.com/Microsoft/TypeScript-TmLanguage) -57. Unicode version 12.0.0 (http://www.unicode.org/) +57. Unicode version 12.0.0 (https://home.unicode.org/) 58. vscode-codicons version 0.0.1 (https://github.com/microsoft/vscode-codicons) -59. vscode-logfile-highlighter version 2.6.0 (https://github.com/emilast/vscode-logfile-highlighter) +59. vscode-logfile-highlighter version 2.8.0 (https://github.com/emilast/vscode-logfile-highlighter) 60. vscode-swift version 0.0.1 (https://github.com/owensd/vscode-swift) 61. Web Background Synchronization (https://github.com/WICG/BackgroundSync) diff --git a/build/azure-pipelines/darwin/continuous-build-darwin.yml b/build/azure-pipelines/darwin/continuous-build-darwin.yml index 5785de63367..447e18e7cb5 100644 --- a/build/azure-pipelines/darwin/continuous-build-darwin.yml +++ b/build/azure-pipelines/darwin/continuous-build-darwin.yml @@ -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..e231ca3d4f2 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -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')) @@ -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/linux/continuous-build-linux.yml b/build/azure-pipelines/linux/continuous-build-linux.yml index fdd4c305cda..3e239caad54 100644 --- a/build/azure-pipelines/linux/continuous-build-linux.yml +++ b/build/azure-pipelines/linux/continuous-build-linux.yml @@ -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.yml b/build/azure-pipelines/linux/product-build-linux.yml index 5d7bccf467f..b164e915d7e 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -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')) @@ -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/mixin.js b/build/azure-pipelines/mixin.js index 8d441e783a9..e133b2d2bb9 100644 --- a/build/azure-pipelines/mixin.js +++ b/build/azure-pipelines/mixin.js @@ -55,7 +55,7 @@ function main() { fancyLog(ansiColors.blue('[mixin]'), 'Inheriting OSS built-in extensions', builtInExtensions.map(e => e.name)); } - return { ...o, builtInExtensions }; + return { webBuiltInExtensions: ossProduct.webBuiltInExtensions, ...o, builtInExtensions }; })) .pipe(productJsonFilter.restore) .pipe(es.mapSync(function (f) { 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/publish-types/update-types.ts b/build/azure-pipelines/publish-types/update-types.ts index a5ef449b77b..c2677d446c6 100644 --- a/build/azure-pipelines/publish-types/update-types.ts +++ b/build/azure-pipelines/publish-types/update-types.ts @@ -36,6 +36,18 @@ function updateDTSFile(outPath: string, tag: string) { fs.writeFileSync(outPath, newContent); } +function repeat(str: string, times: number): string { + const result = new Array(times); + for (let i = 0; i < times; i++) { + result[i] = str; + } + return result.join(''); +} + +function convertTabsToSpaces(str: string): string { + return str.replace(/^\t+/gm, value => repeat(' ', value.length)); +} + function getNewFileContent(content: string, tag: string) { const oldheader = [ `/*---------------------------------------------------------------------------------------------`, @@ -44,7 +56,7 @@ function getNewFileContent(content: string, tag: string) { ` *--------------------------------------------------------------------------------------------*/` ].join('\n'); - return getNewFileHeader(tag) + content.slice(oldheader.length); + return convertTabsToSpaces(getNewFileHeader(tag) + content.slice(oldheader.length)); } function getNewFileHeader(tag: string) { diff --git a/build/azure-pipelines/win32/continuous-build-win32.yml b/build/azure-pipelines/win32/continuous-build-win32.yml index 026a162f510..1be638a4794 100644 --- a/build/azure-pipelines/win32/continuous-build-win32.yml +++ b/build/azure-pipelines/win32/continuous-build-win32.yml @@ -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.yml b/build/azure-pipelines/win32/product-build-win32.yml index fb4f3052578..1c958ddd188 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -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')) @@ -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.extensions.js b/build/gulpfile.extensions.js index 6f7a9e678b4..6ae72e4cf06 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -165,8 +165,8 @@ gulp.task(compileExtensionsBuildLegacyTask); const cleanExtensionsBuildTask = task.define('clean-extensions-build', util.rimraf('.build/extensions')); const compileExtensionsBuildTask = task.define('compile-extensions-build', task.series( cleanExtensionsBuildTask, - task.define('bundle-extensions-build', () => ext.packageLocalExtensionsStream().pipe(gulp.dest('.build'))), - task.define('bundle-marketplace-extensions-build', () => ext.packageMarketplaceExtensionsStream().pipe(gulp.dest('.build'))), + task.define('bundle-extensions-build', () => ext.packageLocalExtensionsStream(false).pipe(gulp.dest('.build'))), + task.define('bundle-marketplace-extensions-build', () => ext.packageMarketplaceExtensionsStream(false).pipe(gulp.dest('.build'))), )); gulp.task(compileExtensionsBuildTask); diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index 2fbd50c18aa..fd1d8ceeed9 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -41,8 +41,8 @@ const indentationFilter = [ '**', // except specific files - '!ThirdPartyNotices.txt', - '!LICENSE.{txt,rtf}', + '!**/ThirdPartyNotices.txt', + '!**/LICENSE.{txt,rtf}', '!LICENSES.chromium.html', '!**/LICENSE', '!src/vs/nls.js', diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index bbb7e60e699..1bf7d36a9f6 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -77,8 +77,8 @@ const vscodeResources = [ 'out-build/vs/platform/files/**/*.md', 'out-build/vs/code/electron-browser/workbench/**', 'out-build/vs/code/electron-browser/sharedProcess/sharedProcess.js', - 'out-build/vs/code/electron-browser/issue/issueReporter.js', - 'out-build/vs/code/electron-browser/processExplorer/processExplorer.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', '!**/test/**' ]; diff --git a/build/lib/builtInExtensions.js b/build/lib/builtInExtensions.js index ebcf8bc8ddb..f86414211ec 100644 --- a/build/lib/builtInExtensions.js +++ b/build/lib/builtInExtensions.js @@ -18,7 +18,9 @@ const fancyLog = require('fancy-log'); const ansiColors = require('ansi-colors'); const root = path.dirname(path.dirname(__dirname)); -const builtInExtensions = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')).builtInExtensions; +const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); +const builtInExtensions = productjson.builtInExtensions; +const webBuiltInExtensions = productjson.webBuiltInExtensions; const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'control.json'); const ENABLE_LOGGING = !process.env['VSCODE_BUILD_BUILTIN_EXTENSIONS_SILENCE_PLEASE']; @@ -100,14 +102,14 @@ function writeControlFile(control) { fs.writeFileSync(controlFilePath, JSON.stringify(control, null, 2)); } -function main() { +exports.getBuiltInExtensions = function getBuiltInExtensions() { log('Syncronizing built-in extensions...'); log(`You can manage built-in extensions with the ${ansiColors.cyan('--builtin')} flag`); const control = readControlFile(); const streams = []; - for (const extension of builtInExtensions) { + for (const extension of [...builtInExtensions, ...webBuiltInExtensions]) { let controlState = control[extension.name] || 'marketplace'; control[extension.name] = controlState; @@ -116,14 +118,16 @@ function main() { writeControlFile(control); - es.merge(streams) - .on('error', err => { - console.error(err); - process.exit(1); - }) - .on('end', () => { - process.exit(0); - }); -} + return new Promise((resolve, reject) => { + es.merge(streams) + .on('error', reject) + .on('end', resolve); + }); +}; -main(); +if (require.main === module) { + exports.getBuiltInExtensions().then(() => process.exit(0)).catch(err => { + console.error(err); + process.exit(1); + }); +} diff --git a/build/lib/extensions.js b/build/lib/extensions.js index d289ccf9107..9cc40c4e1be 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.translatePackageJSON = exports.scanBuiltinExtensions = exports.packageMarketplaceWebExtensionsStream = exports.packageMarketplaceExtensionsStream = exports.packageLocalWebExtensionsStream = exports.packageLocalExtensionsStream = exports.fromMarketplace = void 0; +exports.translatePackageJSON = exports.scanBuiltinExtensions = exports.packageMarketplaceExtensionsStream = exports.packageLocalExtensionsStream = exports.fromMarketplace = void 0; const es = require("event-stream"); const fs = require("fs"); const glob = require("glob"); @@ -22,22 +22,28 @@ const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); const buffer = require('gulp-buffer'); const json = require("gulp-json-editor"); +const jsoncParser = require("jsonc-parser"); const webpack = require('webpack'); const webpackGulp = require('webpack-stream'); const util = require('./util'); const root = path.dirname(path.dirname(__dirname)); const commit = util.getVersion(root); const sourceMappingURLBase = `https://ticino.blob.core.windows.net/sourcemaps/${commit}`; -function minimizeLanguageJSON(input) { - const tmLanguageJsonFilter = filter('**/*.tmLanguage.json', { restore: true }); +function minifyExtensionResources(input) { + const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true }); return input - .pipe(tmLanguageJsonFilter) + .pipe(jsonFilter) .pipe(buffer()) .pipe(es.mapSync((f) => { - f.contents = Buffer.from(JSON.stringify(JSON.parse(f.contents.toString('utf8')))); + const errors = []; + const value = jsoncParser.parse(f.contents.toString('utf8'), errors); + if (errors.length === 0) { + // file parsed OK => just stringify to drop whitespace and comments + f.contents = Buffer.from(JSON.stringify(value)); + } return f; })) - .pipe(tmLanguageJsonFilter.restore); + .pipe(jsonFilter.restore); } function updateExtensionPackageJSON(input, update) { const packageJsonFilter = filter('extensions/*/package.json', { restore: true }); @@ -57,24 +63,18 @@ function fromLocal(extensionPath, forWeb) { let input = isWebPacked ? fromLocalWebpack(extensionPath, webpackConfigFileName) : fromLocalNormal(extensionPath); - if (forWeb) { - input = updateExtensionPackageJSON(input, (data) => { - if (data.browser) { - data.main = data.browser; - } - data.extensionKind = ['web']; - return data; - }); - } - else if (isWebPacked) { + if (isWebPacked) { input = updateExtensionPackageJSON(input, (data) => { + delete data.scripts; + delete data.dependencies; + delete data.devDependencies; if (data.main) { data.main = data.main.replace('/out/', /dist/); } return data; }); } - return minimizeLanguageJSON(input); + return input; } function fromLocalWebpack(extensionPath, webpackConfigFileName) { const result = es.through(); @@ -193,104 +193,111 @@ function fromMarketplace(extensionName, version, metadata) { exports.fromMarketplace = fromMarketplace; const excludedExtensions = [ 'vscode-api-tests', - 'vscode-web-playground', 'vscode-colorize-tests', 'vscode-test-resolver', 'ms-vscode.node-debug', 'ms-vscode.node-debug2', - 'vscode-notebook-tests' + 'vscode-notebook-tests', + 'vscode-custom-editor-tests', ]; -const builtInExtensions = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')).builtInExtensions; -function packageLocalExtensionsStream() { - const localExtensionDescriptions = glob.sync('extensions/*/package.json') +const marketplaceWebExtensions = [ + 'ms-vscode.references-view' +]; +const productJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); +const builtInExtensions = productJson.builtInExtensions || []; +const webBuiltInExtensions = productJson.webBuiltInExtensions || []; +/** + * Loosely based on `getExtensionKind` from `src/vs/workbench/services/extensions/common/extensionsUtil.ts` + */ +function isWebExtension(manifest) { + if (typeof manifest.extensionKind !== 'undefined') { + const extensionKind = Array.isArray(manifest.extensionKind) ? manifest.extensionKind : [manifest.extensionKind]; + return (extensionKind.indexOf('web') >= 0); + } + return (!Boolean(manifest.main) || Boolean(manifest.browser)); +} +function packageLocalExtensionsStream(forWeb) { + const localExtensionsDescriptions = (glob.sync('extensions/*/package.json') .map(manifestPath => { + const absoluteManifestPath = path.join(root, manifestPath); const extensionPath = path.dirname(path.join(root, manifestPath)); const extensionName = path.basename(extensionPath); - return { name: extensionName, path: 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)); - const localExtensions = localExtensionDescriptions.map(extension => { - return fromLocal(extension.path, false) + .filter(({ name }) => builtInExtensions.every(b => b.name !== name)) + .filter(({ manifestPath }) => (forWeb ? isWebExtension(require(manifestPath)) : true))); + const localExtensionsStream = minifyExtensionResources(es.merge(...localExtensionsDescriptions.map(extension => { + return fromLocal(extension.path, forWeb) .pipe(rename(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)); - }); - const nodeModules = gulp.src('extensions/node_modules/**', { base: '.' }); - return es.merge(nodeModules, ...localExtensions) - .pipe(util2.setExecutableBit(['**/*.sh'])); + }))); + let result; + if (forWeb) { + result = localExtensionsStream; + } + else { + // also include shared node modules + result = es.merge(localExtensionsStream, gulp.src('extensions/node_modules/**', { base: '.' })); + } + return (result + .pipe(util2.setExecutableBit(['**/*.sh']))); } exports.packageLocalExtensionsStream = packageLocalExtensionsStream; -function packageLocalWebExtensionsStream() { - const localExtensionDescriptions = glob.sync('extensions/*/package.json') - .filter(manifestPath => { - const packageJsonConfig = require(path.join(root, manifestPath)); - return !packageJsonConfig.main || packageJsonConfig.browser; - }) - .map(manifestPath => { - const extensionPath = path.dirname(path.join(root, manifestPath)); - const extensionName = path.basename(extensionPath); - return { name: extensionName, path: extensionPath }; - }); - return es.merge(...localExtensionDescriptions.map(extension => { - return fromLocal(extension.path, true) - .pipe(rename(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)); - })); -} -exports.packageLocalWebExtensionsStream = packageLocalWebExtensionsStream; -function packageMarketplaceExtensionsStream() { - const extensions = builtInExtensions.map(extension => { - return fromMarketplace(extension.name, extension.version, extension.metadata) - .pipe(rename(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)); - }); - return es.merge(extensions) - .pipe(util2.setExecutableBit(['**/*.sh'])); -} -exports.packageMarketplaceExtensionsStream = packageMarketplaceExtensionsStream; -function packageMarketplaceWebExtensionsStream(builtInExtensions) { - const extensions = builtInExtensions +function packageMarketplaceExtensionsStream(forWeb) { + const marketplaceExtensionsDescriptions = [ + ...builtInExtensions.filter(({ name }) => (forWeb ? marketplaceWebExtensions.indexOf(name) >= 0 : true)), + ...(forWeb ? webBuiltInExtensions : []) + ]; + const marketplaceExtensionsStream = minifyExtensionResources(es.merge(...marketplaceExtensionsDescriptions .map(extension => { const input = fromMarketplace(extension.name, extension.version, extension.metadata) .pipe(rename(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)); return updateExtensionPackageJSON(input, (data) => { - if (data.main) { - data.browser = data.main; - } - data.extensionKind = ['web']; + delete data.scripts; + delete data.dependencies; + delete data.devDependencies; return data; }); - }); - return es.merge(extensions); + }))); + return (marketplaceExtensionsStream + .pipe(util2.setExecutableBit(['**/*.sh']))); } -exports.packageMarketplaceWebExtensionsStream = packageMarketplaceWebExtensionsStream; -function scanBuiltinExtensions(extensionsRoot, forWeb) { +exports.packageMarketplaceExtensionsStream = packageMarketplaceExtensionsStream; +function scanBuiltinExtensions(extensionsRoot, exclude = []) { const scannedExtensions = []; - const extensionsFolders = fs.readdirSync(extensionsRoot); - for (const extensionFolder of extensionsFolders) { - const packageJSONPath = path.join(extensionsRoot, extensionFolder, 'package.json'); - if (!fs.existsSync(packageJSONPath)) { - continue; + try { + const extensionsFolders = fs.readdirSync(extensionsRoot); + for (const extensionFolder of extensionsFolders) { + if (exclude.indexOf(extensionFolder) >= 0) { + continue; + } + const packageJSONPath = path.join(extensionsRoot, extensionFolder, 'package.json'); + if (!fs.existsSync(packageJSONPath)) { + continue; + } + let packageJSON = JSON.parse(fs.readFileSync(packageJSONPath).toString('utf8')); + if (!isWebExtension(packageJSON)) { + continue; + } + const children = fs.readdirSync(path.join(extensionsRoot, extensionFolder)); + const packageNLSPath = children.filter(child => child === 'package.nls.json')[0]; + const packageNLS = packageNLSPath ? JSON.parse(fs.readFileSync(path.join(extensionsRoot, extensionFolder, packageNLSPath)).toString()) : undefined; + const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0]; + const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0]; + scannedExtensions.push({ + extensionPath: extensionFolder, + packageJSON, + packageNLS, + readmePath: readme ? path.join(extensionFolder, readme) : undefined, + changelogPath: changelog ? path.join(extensionFolder, changelog) : undefined, + }); } - let packageJSON = JSON.parse(fs.readFileSync(packageJSONPath).toString('utf8')); - const extensionKind = packageJSON['extensionKind'] || []; - if (forWeb && extensionKind.indexOf('web') === -1) { - continue; - } - const children = fs.readdirSync(path.join(extensionsRoot, extensionFolder)); - const packageNLS = children.filter(child => child === 'package.nls.json')[0]; - const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0]; - const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0]; - if (packageNLS) { - // temporary - packageJSON = translatePackageJSON(packageJSON, path.join(extensionsRoot, extensionFolder, packageNLS)); - } - scannedExtensions.push({ - extensionPath: extensionFolder, - packageJSON, - packageNLSPath: packageNLS ? path.join(extensionFolder, packageNLS) : undefined, - readmePath: readme ? path.join(extensionFolder, readme) : undefined, - changelogPath: changelog ? path.join(extensionFolder, changelog) : undefined, - }); + return scannedExtensions; + } + catch (ex) { + return scannedExtensions; } - return scannedExtensions; } exports.scanBuiltinExtensions = scanBuiltinExtensions; function translatePackageJSON(packageJSON, packageNLSPath) { diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 3810723d7d4..7e529f17cb8 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -21,6 +21,7 @@ import * as fancyLog from 'fancy-log'; import * as ansiColors from 'ansi-colors'; const buffer = require('gulp-buffer'); import json = require('gulp-json-editor'); +import * as jsoncParser from 'jsonc-parser'; const webpack = require('webpack'); const webpackGulp = require('webpack-stream'); const util = require('./util'); @@ -28,16 +29,21 @@ const root = path.dirname(path.dirname(__dirname)); const commit = util.getVersion(root); const sourceMappingURLBase = `https://ticino.blob.core.windows.net/sourcemaps/${commit}`; -function minimizeLanguageJSON(input: Stream): Stream { - const tmLanguageJsonFilter = filter('**/*.tmLanguage.json', { restore: true }); +function minifyExtensionResources(input: Stream): Stream { + const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true }); return input - .pipe(tmLanguageJsonFilter) + .pipe(jsonFilter) .pipe(buffer()) .pipe(es.mapSync((f: File) => { - f.contents = Buffer.from(JSON.stringify(JSON.parse(f.contents.toString('utf8')))); + const errors: jsoncParser.ParseError[] = []; + const value = jsoncParser.parse(f.contents.toString('utf8'), errors); + if (errors.length === 0) { + // file parsed OK => just stringify to drop whitespace and comments + f.contents = Buffer.from(JSON.stringify(value)); + } return f; })) - .pipe(tmLanguageJsonFilter.restore); + .pipe(jsonFilter.restore); } function updateExtensionPackageJSON(input: Stream, update: (data: any) => any): Stream { @@ -61,16 +67,11 @@ function fromLocal(extensionPath: string, forWeb: boolean): Stream { ? fromLocalWebpack(extensionPath, webpackConfigFileName) : fromLocalNormal(extensionPath); - if (forWeb) { - input = updateExtensionPackageJSON(input, (data: any) => { - if (data.browser) { - data.main = data.browser; - } - data.extensionKind = ['web']; - return data; - }); - } else if (isWebPacked) { + if (isWebPacked) { input = updateExtensionPackageJSON(input, (data: any) => { + delete data.scripts; + delete data.dependencies; + delete data.devDependencies; if (data.main) { data.main = data.main.replace('/out/', /dist/); } @@ -78,7 +79,7 @@ function fromLocal(extensionPath: string, forWeb: boolean): Stream { }); } - return minimizeLanguageJSON(input); + return input; } @@ -223,15 +224,18 @@ export function fromMarketplace(extensionName: string, version: string, metadata .pipe(json({ __metadata: metadata })) .pipe(packageJsonFilter.restore); } - const excludedExtensions = [ 'vscode-api-tests', - 'vscode-web-playground', 'vscode-colorize-tests', 'vscode-test-resolver', 'ms-vscode.node-debug', 'ms-vscode.node-debug2', - 'vscode-notebook-tests' + 'vscode-notebook-tests', + 'vscode-custom-editor-tests', +]; + +const marketplaceWebExtensions = [ + 'ms-vscode.references-view' ]; interface IBuiltInExtension { @@ -241,112 +245,134 @@ interface IBuiltInExtension { metadata: any; } -const builtInExtensions: IBuiltInExtension[] = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')).builtInExtensions; +const productJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); +const builtInExtensions: IBuiltInExtension[] = productJson.builtInExtensions || []; +const webBuiltInExtensions: IBuiltInExtension[] = productJson.webBuiltInExtensions || []; -export function packageLocalExtensionsStream(): NodeJS.ReadWriteStream { - const localExtensionDescriptions = (glob.sync('extensions/*/package.json')) - .map(manifestPath => { - const extensionPath = path.dirname(path.join(root, manifestPath)); - const extensionName = path.basename(extensionPath); - return { name: extensionName, path: extensionPath }; - }) - .filter(({ name }) => excludedExtensions.indexOf(name) === -1) - .filter(({ name }) => builtInExtensions.every(b => b.name !== name)); - - - const localExtensions = localExtensionDescriptions.map(extension => { - return fromLocal(extension.path, false) - .pipe(rename(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)); - }); - - const nodeModules = gulp.src('extensions/node_modules/**', { base: '.' }); - return es.merge(nodeModules, ...localExtensions) - .pipe(util2.setExecutableBit(['**/*.sh'])); +type ExtensionKind = 'ui' | 'workspace' | 'web'; +interface IExtensionManifest { + main: string; + browser: string; + extensionKind?: ExtensionKind | ExtensionKind[]; +} +/** + * Loosely based on `getExtensionKind` from `src/vs/workbench/services/extensions/common/extensionsUtil.ts` + */ +function isWebExtension(manifest: IExtensionManifest): boolean { + if (typeof manifest.extensionKind !== 'undefined') { + const extensionKind = Array.isArray(manifest.extensionKind) ? manifest.extensionKind : [manifest.extensionKind]; + return (extensionKind.indexOf('web') >= 0); + } + return (!Boolean(manifest.main) || Boolean(manifest.browser)); } -export function packageLocalWebExtensionsStream(): NodeJS.ReadWriteStream { - const localExtensionDescriptions = (glob.sync('extensions/*/package.json')) - .filter(manifestPath => { - const packageJsonConfig = require(path.join(root, manifestPath)); - return !packageJsonConfig.main || packageJsonConfig.browser; - }) - .map(manifestPath => { - const extensionPath = path.dirname(path.join(root, manifestPath)); - const extensionName = path.basename(extensionPath); - return { name: extensionName, path: extensionPath }; - }); +export function packageLocalExtensionsStream(forWeb: boolean): Stream { + const localExtensionsDescriptions = ( + (glob.sync('extensions/*/package.json')) + .map(manifestPath => { + const absoluteManifestPath = path.join(root, manifestPath); + const extensionPath = path.dirname(path.join(root, manifestPath)); + 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)) + ); + const localExtensionsStream = minifyExtensionResources( + es.merge( + ...localExtensionsDescriptions.map(extension => { + return fromLocal(extension.path, forWeb) + .pipe(rename(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)); + }) + ) + ); - return es.merge(...localExtensionDescriptions.map(extension => { - return fromLocal(extension.path, true) - .pipe(rename(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)); - })); + let result: Stream; + if (forWeb) { + result = localExtensionsStream; + } else { + // also include shared node modules + result = es.merge(localExtensionsStream, gulp.src('extensions/node_modules/**', { base: '.' })); + } + + return ( + result + .pipe(util2.setExecutableBit(['**/*.sh'])) + ); } -export function packageMarketplaceExtensionsStream(): NodeJS.ReadWriteStream { - const extensions = builtInExtensions.map(extension => { - return fromMarketplace(extension.name, extension.version, extension.metadata) - .pipe(rename(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)); - }); +export function packageMarketplaceExtensionsStream(forWeb: boolean): Stream { + const marketplaceExtensionsDescriptions = [ + ...builtInExtensions.filter(({ name }) => (forWeb ? marketplaceWebExtensions.indexOf(name) >= 0 : true)), + ...(forWeb ? webBuiltInExtensions : []) + ]; + const marketplaceExtensionsStream = minifyExtensionResources( + es.merge( + ...marketplaceExtensionsDescriptions + .map(extension => { + const input = fromMarketplace(extension.name, extension.version, extension.metadata) + .pipe(rename(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)); + return updateExtensionPackageJSON(input, (data: any) => { + delete data.scripts; + delete data.dependencies; + delete data.devDependencies; + return data; + }); + }) + ) + ); - return es.merge(extensions) - .pipe(util2.setExecutableBit(['**/*.sh'])); -} - -export function packageMarketplaceWebExtensionsStream(builtInExtensions: IBuiltInExtension[]): NodeJS.ReadWriteStream { - const extensions = builtInExtensions - .map(extension => { - const input = fromMarketplace(extension.name, extension.version, extension.metadata) - .pipe(rename(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)); - return updateExtensionPackageJSON(input, (data: any) => { - if (data.main) { - data.browser = data.main; - } - data.extensionKind = ['web']; - return data; - }); - }); - return es.merge(extensions); + return ( + marketplaceExtensionsStream + .pipe(util2.setExecutableBit(['**/*.sh'])) + ); } export interface IScannedBuiltinExtension { - extensionPath: string, - packageJSON: any, - packageNLSPath?: string, - readmePath?: string, - changelogPath?: string, + extensionPath: string; + packageJSON: any; + packageNLS?: any; + readmePath?: string; + changelogPath?: string; } -export function scanBuiltinExtensions(extensionsRoot: string, forWeb: boolean): IScannedBuiltinExtension[] { +export function scanBuiltinExtensions(extensionsRoot: string, exclude: string[] = []): IScannedBuiltinExtension[] { const scannedExtensions: IScannedBuiltinExtension[] = []; - const extensionsFolders = fs.readdirSync(extensionsRoot); - for (const extensionFolder of extensionsFolders) { - const packageJSONPath = path.join(extensionsRoot, extensionFolder, 'package.json'); - if (!fs.existsSync(packageJSONPath)) { - continue; - } - let packageJSON = JSON.parse(fs.readFileSync(packageJSONPath).toString('utf8')); - const extensionKind: string[] = packageJSON['extensionKind'] || []; - if (forWeb && extensionKind.indexOf('web') === -1) { - continue; - } - const children = fs.readdirSync(path.join(extensionsRoot, extensionFolder)); - const packageNLS = children.filter(child => child === 'package.nls.json')[0]; - const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0]; - const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0]; - if (packageNLS) { - // temporary - packageJSON = translatePackageJSON(packageJSON, path.join(extensionsRoot, extensionFolder, packageNLS)) + try { + const extensionsFolders = fs.readdirSync(extensionsRoot); + for (const extensionFolder of extensionsFolders) { + if (exclude.indexOf(extensionFolder) >= 0) { + continue; + } + const packageJSONPath = path.join(extensionsRoot, extensionFolder, 'package.json'); + if (!fs.existsSync(packageJSONPath)) { + continue; + } + let packageJSON = JSON.parse(fs.readFileSync(packageJSONPath).toString('utf8')); + if (!isWebExtension(packageJSON)) { + continue; + } + const children = fs.readdirSync(path.join(extensionsRoot, extensionFolder)); + const packageNLSPath = children.filter(child => child === 'package.nls.json')[0]; + const packageNLS = packageNLSPath ? JSON.parse(fs.readFileSync(path.join(extensionsRoot, extensionFolder, packageNLSPath)).toString()) : undefined; + const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0]; + const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0]; + + scannedExtensions.push({ + extensionPath: extensionFolder, + packageJSON, + packageNLS, + readmePath: readme ? path.join(extensionFolder, readme) : undefined, + changelogPath: changelog ? path.join(extensionFolder, changelog) : undefined, + }); } - scannedExtensions.push({ - extensionPath: extensionFolder, - packageJSON, - packageNLSPath: packageNLS ? path.join(extensionFolder, packageNLS) : undefined, - readmePath: readme ? path.join(extensionFolder, readme) : undefined, - changelogPath: changelog ? path.join(extensionFolder, changelog) : undefined, - }); + return scannedExtensions; + } catch (ex) { + return scannedExtensions; } - return scannedExtensions; } export function translatePackageJSON(packageJSON: string, packageNLSPath: string) { diff --git a/build/lib/optimize.js b/build/lib/optimize.js index 5015f76649b..5403c105620 100644 --- a/build/lib/optimize.js +++ b/build/lib/optimize.js @@ -70,7 +70,7 @@ function loader(src, bundledFileHeader, bundleLoader) { })) .pipe(concat('vs/loader.js'))); } -function toConcatStream(src, bundledFileHeader, sources, dest) { +function toConcatStream(src, bundledFileHeader, sources, dest, fileContentMapper) { const useSourcemaps = /\.js$/.test(dest) && !/\.nls\.js$/.test(dest); // If a bundle ends up including in any of the sources our copyright, then // insert a fake source at the beginning of each bundle with our copyright @@ -91,10 +91,12 @@ function toConcatStream(src, bundledFileHeader, sources, dest) { const treatedSources = sources.map(function (source) { const root = source.path ? REPO_ROOT_PATH.replace(/\\/g, '/') : ''; const base = source.path ? root + `/${src}` : ''; + const path = source.path ? root + '/' + source.path.replace(/\\/g, '/') : 'fake'; + const contents = source.path ? fileContentMapper(source.contents, path) : source.contents; return new VinylFile({ - path: source.path ? root + '/' + source.path.replace(/\\/g, '/') : 'fake', + path: path, base: base, - contents: Buffer.from(source.contents) + contents: Buffer.from(contents) }); }); return es.readArray(treatedSources) @@ -102,9 +104,9 @@ function toConcatStream(src, bundledFileHeader, sources, dest) { .pipe(concat(dest)) .pipe(stats_1.createStatsStream(dest)); } -function toBundleStream(src, bundledFileHeader, bundles) { +function toBundleStream(src, bundledFileHeader, bundles, fileContentMapper) { return es.merge(bundles.map(function (bundle) { - return toConcatStream(src, bundledFileHeader, bundle.sources, bundle.dest); + return toConcatStream(src, bundledFileHeader, bundle.sources, bundle.dest, fileContentMapper); })); } const DEFAULT_FILE_HEADER = [ @@ -120,6 +122,7 @@ function optimizeTask(opts) { const bundledFileHeader = opts.header || DEFAULT_FILE_HEADER; const bundleLoader = (typeof opts.bundleLoader === 'undefined' ? true : opts.bundleLoader); const out = opts.out; + const fileContentMapper = opts.fileContentMapper || ((contents, _path) => contents); return function () { const bundlesStream = es.through(); // this stream will contain the bundled files const resourcesStream = es.through(); // this stream will contain the resources @@ -128,7 +131,7 @@ function optimizeTask(opts) { if (err || !result) { return bundlesStream.emit('error', JSON.stringify(err)); } - toBundleStream(src, bundledFileHeader, result.files).pipe(bundlesStream); + toBundleStream(src, bundledFileHeader, result.files, fileContentMapper).pipe(bundlesStream); // Remove css inlined resources const filteredResources = resources.slice(); result.cssInlinedResources.forEach(function (resource) { diff --git a/build/lib/optimize.ts b/build/lib/optimize.ts index 1fd00c15cb9..2822803f9f7 100644 --- a/build/lib/optimize.ts +++ b/build/lib/optimize.ts @@ -18,7 +18,6 @@ import * as fancyLog from 'fancy-log'; import * as ansiColors from 'ansi-colors'; import * as path from 'path'; import * as pump from 'pump'; -import * as sm from 'source-map'; import * as terser from 'terser'; import * as VinylFile from 'vinyl'; import * as bundle from './bundle'; @@ -48,10 +47,6 @@ export function loaderConfig() { const IS_OUR_COPYRIGHT_REGEXP = /Copyright \(C\) Microsoft Corporation/i; -declare class FileSourceMap extends VinylFile { - public sourceMap: sm.RawSourceMap; -} - function loader(src: string, bundledFileHeader: string, bundleLoader: boolean): NodeJS.ReadWriteStream { let sources = [ `${src}/vs/loader.js` @@ -84,7 +79,7 @@ function loader(src: string, bundledFileHeader: string, bundleLoader: boolean): ); } -function toConcatStream(src: string, bundledFileHeader: string, sources: bundle.IFile[], dest: string): NodeJS.ReadWriteStream { +function toConcatStream(src: string, bundledFileHeader: string, sources: bundle.IFile[], dest: string, fileContentMapper: (contents: string, path: string) => string): NodeJS.ReadWriteStream { const useSourcemaps = /\.js$/.test(dest) && !/\.nls\.js$/.test(dest); // If a bundle ends up including in any of the sources our copyright, then @@ -108,11 +103,13 @@ function toConcatStream(src: string, bundledFileHeader: string, sources: bundle. const treatedSources = sources.map(function (source) { const root = source.path ? REPO_ROOT_PATH.replace(/\\/g, '/') : ''; const base = source.path ? root + `/${src}` : ''; + const path = source.path ? root + '/' + source.path.replace(/\\/g, '/') : 'fake'; + const contents = source.path ? fileContentMapper(source.contents, path) : source.contents; return new VinylFile({ - path: source.path ? root + '/' + source.path.replace(/\\/g, '/') : 'fake', + path: path, base: base, - contents: Buffer.from(source.contents) + contents: Buffer.from(contents) }); }); @@ -122,9 +119,9 @@ function toConcatStream(src: string, bundledFileHeader: string, sources: bundle. .pipe(createStatsStream(dest)); } -function toBundleStream(src: string, bundledFileHeader: string, bundles: bundle.IConcatFile[]): NodeJS.ReadWriteStream { +function toBundleStream(src: string, bundledFileHeader: string, bundles: bundle.IConcatFile[], fileContentMapper: (contents: string, path: string) => string): NodeJS.ReadWriteStream { return es.merge(bundles.map(function (bundle) { - return toConcatStream(src, bundledFileHeader, bundle.sources, bundle.dest); + return toConcatStream(src, bundledFileHeader, bundle.sources, bundle.dest, fileContentMapper); })); } @@ -162,6 +159,12 @@ export interface IOptimizeTaskOpts { * (out folder name) */ languages?: Language[]; + /** + * File contents interceptor + * @param contents The contens of the file + * @param path The absolute file path, always using `/`, even on Windows + */ + fileContentMapper?: (contents: string, path: string) => string; } const DEFAULT_FILE_HEADER = [ @@ -178,6 +181,7 @@ export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStr const bundledFileHeader = opts.header || DEFAULT_FILE_HEADER; const bundleLoader = (typeof opts.bundleLoader === 'undefined' ? true : opts.bundleLoader); const out = opts.out; + const fileContentMapper = opts.fileContentMapper || ((contents: string, _path: string) => contents); return function () { const bundlesStream = es.through(); // this stream will contain the bundled files @@ -187,7 +191,7 @@ export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStr bundle.bundle(entryPoints, loaderConfig, function (err, result) { if (err || !result) { return bundlesStream.emit('error', JSON.stringify(err)); } - toBundleStream(src, bundledFileHeader, result.files).pipe(bundlesStream); + toBundleStream(src, bundledFileHeader, result.files, fileContentMapper).pipe(bundlesStream); // Remove css inlined resources const filteredResources = resources.slice(); diff --git a/build/lib/preLaunch.js b/build/lib/preLaunch.js new file mode 100644 index 00000000000..1aecbe19048 --- /dev/null +++ b/build/lib/preLaunch.js @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +// @ts-check +const path = require("path"); +const child_process_1 = require("child_process"); +const fs_1 = require("fs"); +const yarn = process.platform === 'win32' ? 'yarn.cmd' : 'yarn'; +const rootDir = path.resolve(__dirname, '..', '..'); +function runProcess(command, args = []) { + return new Promise((resolve, reject) => { + const child = child_process_1.spawn(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env }); + child.on('exit', err => !err ? resolve() : process.exit(err !== null && err !== void 0 ? err : 1)); + child.on('error', reject); + }); +} +async function exists(subdir) { + try { + await fs_1.promises.stat(path.join(rootDir, subdir)); + return true; + } + catch (_a) { + return false; + } +} +async function ensureNodeModules() { + if (!(await exists('node_modules'))) { + await runProcess(yarn); + } +} +async function getElectron() { + await runProcess(yarn, ['electron']); +} +async function ensureCompiled() { + if (!(await exists('out'))) { + await runProcess(yarn, ['compile']); + } +} +async function main() { + await ensureNodeModules(); + await getElectron(); + await ensureCompiled(); + // Can't require this until after dependencies are installed + const { getBuiltInExtensions } = require('./builtInExtensions'); + await getBuiltInExtensions(); +} +if (require.main === module) { + main().catch(err => { + console.error(err); + process.exit(1); + }); +} diff --git a/build/lib/preLaunch.ts b/build/lib/preLaunch.ts new file mode 100644 index 00000000000..bd084f5fec5 --- /dev/null +++ b/build/lib/preLaunch.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +// @ts-check + +import * as path from 'path'; +import { spawn } from 'child_process'; +import { promises as fs } from 'fs'; + +const yarn = process.platform === 'win32' ? 'yarn.cmd' : 'yarn'; +const rootDir = path.resolve(__dirname, '..', '..'); + +function runProcess(command: string, args: ReadonlyArray = []) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env }); + child.on('exit', err => !err ? resolve() : process.exit(err ?? 1)); + child.on('error', reject); + }); +} + +async function exists(subdir: string) { + try { + await fs.stat(path.join(rootDir, subdir)); + return true; + } catch { + return false; + } +} + +async function ensureNodeModules() { + if (!(await exists('node_modules'))) { + await runProcess(yarn); + } +} + +async function getElectron() { + await runProcess(yarn, ['electron']); +} + +async function ensureCompiled() { + if (!(await exists('out'))) { + await runProcess(yarn, ['compile']); + } +} + +async function main() { + await ensureNodeModules(); + await getElectron(); + await ensureCompiled(); + + // Can't require this until after dependencies are installed + const { getBuiltInExtensions } = require('./builtInExtensions'); + await getBuiltInExtensions(); +} + +if (require.main === module) { + main().catch(err => { + console.error(err); + process.exit(1); + }); +} diff --git a/build/package.json b/build/package.json index 3f779c55379..08f2bf30756 100644 --- a/build/package.json +++ b/build/package.json @@ -39,12 +39,13 @@ "gulp-sourcemaps": "^1.11.0", "gulp-uglify": "^3.0.0", "iconv-lite-umd": "0.6.8", + "jsonc-parser": "^2.3.0", "mime": "^1.3.4", "minimatch": "3.0.4", "minimist": "^1.2.3", "request": "^2.85.0", "terser": "4.3.8", - "typescript": "^4.0.0-dev.20200715", + "typescript": "^4.0.0-dev.20200803", "vsce": "1.48.0", "vscode-telemetry-extractor": "^1.6.0", "xml2js": "^0.4.17" diff --git a/build/tsconfig.json b/build/tsconfig.json index df15ccdd1be..f075fe3d4f5 100644 --- a/build/tsconfig.json +++ b/build/tsconfig.json @@ -14,7 +14,8 @@ "checkJs": true, "strict": true, "noUnusedLocals": true, - "noUnusedParameters": true + "noUnusedParameters": true, + "newLine": "lf" }, "include": [ "**/*.ts" diff --git a/build/yarn.lock b/build/yarn.lock index a0ac8823544..4aa0234e59f 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -1600,6 +1600,11 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= +jsonc-parser@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.0.tgz#7c7fc988ee1486d35734faaaa866fadb00fa91ee" + integrity sha512-b0EBt8SWFNnixVdvoR2ZtEGa9ZqLhbJnOjezn+WP+8kspFm+PFYDN8Z4Bc7pRlDjvuVcADSUkroIuTWWn/YiIA== + jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" @@ -1749,9 +1754,9 @@ lodash.unescape@4.0.1: integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw= lodash@^4.15.0, lodash@^4.17.10: - version "4.17.11" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" - integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== macos-release@^2.2.0: version "2.3.0" @@ -2530,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.20200715: - version "4.0.0-dev.20200715" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.0-dev.20200715.tgz#d65961a5a6f13fde95a6f4db5f5946f15e4c59bc" - integrity sha512-gmPXoWktfXeutmWTM6el9U4vIn5kqOHGI1OESSOhPtLWrxodKqLfFuMygQtOUTtGjKLFQRFAJhHEwUhHZNOURA== +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== typical@^4.0.0: version "4.0.0" diff --git a/cglicenses.json b/cglicenses.json index f2d5a39a2b1..0da22bd9f57 100644 --- a/cglicenses.json +++ b/cglicenses.json @@ -330,5 +330,34 @@ "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", "SOFTWARE." ] + }, + { + // Reason: The license at https://github.com/colorjs/color-name/blob/master/LICENSE + // cannot be found by the OSS tool automatically. + "name": "color-name", + "fullLicenseText": [ + "The MIT License (MIT)", + "Copyright (c) 2015 Dmitry Ivanov", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." + ] + }, + { + // Reason: The license cannot be found by the tool due to access controls on the repository + "name": "tas-client", + "fullLicenseText": [ + "MIT License", + "Copyright (c) 2020 - present Microsoft Corporation", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." + ] } ] diff --git a/cgmanifest.json b/cgmanifest.json index 1fb78691e71..cb9954628dd 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "052d3b44972e6d94ef40054d46c150b7cdd7a5d8" + "commitHash": "e4745133a1d3745f066e068b8033c6a269b59caf" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "80.0.3987.165" + "version": "78.0.3904.130" }, { "component": { @@ -48,11 +48,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "42cce5a9d0fd905bf4ad7a2528c36572dfb8b5ad" + "commitHash": "787378879acfb212ed4ff824bf9f767a24a5cb43a" } }, "isOnlyProductionDependency": true, - "version": "12.13.0" + "version": "12.8.1" }, { "component": { @@ -60,12 +60,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "87fd06bc96bce8f46ca05b8315657fd230bcac85" + "commitHash": "5f93e889020d279d5a9cd1ecab080ab467312447" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "8.3.3" + "version": "7.3.2" }, { "component": { diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 477d530a0ed..3cadb80fd33 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -54,7 +54,7 @@ "url": "vscode://schemas/keybindings" }, { - "fileMatch": "vscode://defaultsettings/*/*.json", + "fileMatch": "vscode://defaultsettings/defaultSettings.json", "url": "vscode://schemas/settings/default" }, { diff --git a/extensions/configuration-editing/schemas/attachContainer.schema.json b/extensions/configuration-editing/schemas/attachContainer.schema.json index da06cb1c9ff..fc53bb896f1 100644 --- a/extensions/configuration-editing/schemas/attachContainer.schema.json +++ b/extensions/configuration-editing/schemas/attachContainer.schema.json @@ -46,6 +46,15 @@ "errorMessage": "Expected format: '${publisher}.${name}' or '${publisher}.${name}@${version}'. Example: 'ms-dotnettools.csharp'." } }, + "userEnvProbe": { + "type": "string", + "enum": [ + "none", + "loginInteractiveShell", + "interactiveShell" + ], + "description": "User environment probe to run. The default is none." + }, "postAttachCommand": { "type": [ "string", diff --git a/extensions/configuration-editing/schemas/devContainer.schema.json b/extensions/configuration-editing/schemas/devContainer.schema.json index 596672dede5..999a50e7788 100644 --- a/extensions/configuration-editing/schemas/devContainer.schema.json +++ b/extensions/configuration-editing/schemas/devContainer.schema.json @@ -92,6 +92,15 @@ "type": "integer", "description": "The port VS Code can use to connect to its backend." }, + "userEnvProbe": { + "type": "string", + "enum": [ + "none", + "loginInteractiveShell", + "interactiveShell" + ], + "description": "User environment probe to run. The default is none." + }, "codespaces": { "type": "object", "description": "Codespaces-specific configuration." diff --git a/extensions/configuration-editing/src/settingsDocumentHelper.ts b/extensions/configuration-editing/src/settingsDocumentHelper.ts index 4a7f80c2a2a..5e466c2eb6f 100644 --- a/extensions/configuration-editing/src/settingsDocumentHelper.ts +++ b/extensions/configuration-editing/src/settingsDocumentHelper.ts @@ -42,11 +42,11 @@ export class SettingsDocument { }); } - // sync.ignoredExtensions - if (location.path[0] === 'sync.ignoredExtensions') { + // settingsSync.ignoredExtensions + if (location.path[0] === 'settingsSync.ignoredExtensions') { let ignoredExtensions = []; try { - ignoredExtensions = parse(this.document.getText())['sync.ignoredExtensions']; + ignoredExtensions = parse(this.document.getText())['settingsSync.ignoredExtensions']; } catch (e) {/* ignore error */ } return provideInstalledExtensionProposals(ignoredExtensions, range, true); } @@ -223,7 +223,7 @@ export class SettingsDocument { if (location.path.length === 1 && location.previousNode && typeof location.previousNode.value === 'string' && location.previousNode.value.startsWith('[')) { // Suggestion model word matching includes closed sqaure bracket and ending quote // Hence include them in the proposal to replace - let range = this.document.getWordRangeAtPosition(position) || new vscode.Range(position, position); + const range = this.document.getWordRangeAtPosition(position) || new vscode.Range(position, position); return this.provideLanguageCompletionItemsForLanguageOverrides(location, range, language => `"[${language}]"`); } return Promise.resolve([]); diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index 4c882c8f8b8..ccbdba90a29 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.0", + "vscode-css-languageservice": "^4.3.1", "vscode-languageserver": "7.0.0-next.3", "vscode-uri": "^2.1.2" }, diff --git a/extensions/css-language-features/server/yarn.lock b/extensions/css-language-features/server/yarn.lock index 0a0c3c319cb..3b9a29250f5 100644 --- a/extensions/css-language-features/server/yarn.lock +++ b/extensions/css-language-features/server/yarn.lock @@ -414,15 +414,10 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" -lodash@^4.16.4: - version "4.17.10" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" - integrity sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg== - -lodash@^4.17.15: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== +lodash@^4.16.4, lodash@^4.17.15: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== log-symbols@3.0.0: version "3.0.0" @@ -701,10 +696,10 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -vscode-css-languageservice@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.3.0.tgz#40c797d664ab6188cace33cfbb19b037580a9318" - integrity sha512-BkQAMz4oVHjr0oOAz5PdeE72txlLQK7NIwzmclfr+b6fj6I8POwB+VoXvrZLTbWt9hWRgfvgiQRkh5JwrjPJ5A== +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== dependencies: vscode-languageserver-textdocument "^1.0.1" vscode-languageserver-types "3.16.0-next.2" diff --git a/extensions/docker/package.json b/extensions/docker/package.json index c1b91dc283d..3af7727a6b4 100644 --- a/extensions/docker/package.json +++ b/extensions/docker/package.json @@ -14,6 +14,7 @@ "id": "dockerfile", "extensions": [ ".dockerfile", ".containerfile" ], "filenames": [ "Dockerfile", "Containerfile" ], + "filenamePatterns": [ "Dockerfile.*", "Containerfile.*" ], "aliases": [ "Dockerfile", "Containerfile" ], "configuration": "./language-configuration.json" }], diff --git a/extensions/emmet/.vscodeignore b/extensions/emmet/.vscodeignore index 573d91ebe6b..8180a27356e 100644 --- a/extensions/emmet/.vscodeignore +++ b/extensions/emmet/.vscodeignore @@ -3,7 +3,8 @@ src/** out/** tsconfig.json extension.webpack.config.js +extension-browser.webpack.config.js CONTRIBUTING.md cgmanifest.json yarn.lock -.vscode \ No newline at end of file +.vscode diff --git a/extensions/emmet/yarn.lock b/extensions/emmet/yarn.lock index ca01c59ff1c..06a8845658c 100644 --- a/extensions/emmet/yarn.lock +++ b/extensions/emmet/yarn.lock @@ -1503,9 +1503,9 @@ lodash.values@~2.4.1: lodash.keys "~2.4.1" lodash@^4.16.4: - version "4.17.10" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" - integrity sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg== + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== loud-rejection@^1.0.0: version "1.6.0" diff --git a/extensions/extension-editing/.vscodeignore b/extensions/extension-editing/.vscodeignore index 9d384dd9061..de8e6dc5913 100644 --- a/extensions/extension-editing/.vscodeignore +++ b/extensions/extension-editing/.vscodeignore @@ -3,4 +3,5 @@ src/** tsconfig.json out/** extension.webpack.config.js -yarn.lock \ No newline at end of file +extension-browser.webpack.config.js +yarn.lock diff --git a/extensions/git/package.json b/extensions/git/package.json index e96ef36abab..748868778dd 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -753,149 +753,49 @@ "group": "navigation", "when": "scmProvider == git" }, - { - "command": "git.sync", - "group": "1_sync", - "when": "scmProvider == git" - }, - { - "command": "git.syncRebase", - "group": "1_sync", - "when": "scmProvider == git && gitState == idle" - }, - { - "command": "git.pull", - "group": "1_sync", - "when": "scmProvider == git" - }, - { - "command": "git.pullRebase", - "group": "1_sync", - "when": "scmProvider == git" - }, - { - "command": "git.pullFrom", - "group": "1_sync", - "when": "scmProvider == git" - }, - { - "command": "git.push", - "group": "1_sync", - "when": "scmProvider == git" - }, - { - "command": "git.pushForce", - "group": "1_sync", - "when": "scmProvider == git && config.git.allowForcePush" - }, - { - "command": "git.pushTo", - "group": "1_sync", - "when": "scmProvider == git" - }, - { - "command": "git.pushToForce", - "group": "1_sync", - "when": "scmProvider == git && config.git.allowForcePush" - }, { "command": "git.checkout", - "group": "2_branch", + "group": "1_header", "when": "scmProvider == git" }, { - "command": "git.publish", - "group": "2_branch", + "command": "git.clone", + "group": "1_header", "when": "scmProvider == git" }, { - "command": "git.commitStaged", - "group": "4_commit", + "submenu": "git.commit", + "group": "2_main@1", "when": "scmProvider == git" }, { - "command": "git.commitStagedSigned", - "group": "4_commit", + "submenu": "git.changes", + "group": "2_main@2", "when": "scmProvider == git" }, { - "command": "git.commitStagedAmend", - "group": "4_commit", + "submenu": "git.pullpush", + "group": "2_main@3", "when": "scmProvider == git" }, { - "command": "git.commitAll", - "group": "4_commit", + "submenu": "git.branch", + "group": "2_main@4", "when": "scmProvider == git" }, { - "command": "git.commitAllSigned", - "group": "4_commit", + "submenu": "git.remotes", + "group": "2_main@5", "when": "scmProvider == git" }, { - "command": "git.commitAllAmend", - "group": "4_commit", - "when": "scmProvider == git" - }, - { - "command": "git.undoCommit", - "group": "4_commit", - "when": "scmProvider == git" - }, - { - "command": "git.stageAll", - "group": "5_stage", - "when": "scmProvider == git" - }, - { - "command": "git.unstageAll", - "group": "5_stage", - "when": "scmProvider == git" - }, - { - "command": "git.cleanAll", - "group": "5_stage", - "when": "scmProvider == git" - }, - { - "command": "git.stashIncludeUntracked", - "group": "6_stash", - "when": "scmProvider == git" - }, - { - "command": "git.stash", - "group": "6_stash", - "when": "scmProvider == git" - }, - { - "command": "git.stashPop", - "group": "6_stash", - "when": "scmProvider == git" - }, - { - "command": "git.stashPopLatest", - "group": "6_stash", - "when": "scmProvider == git" - }, - { - "command": "git.stashApply", - "group": "6_stash", - "when": "scmProvider == git" - }, - { - "command": "git.stashApplyLatest", - "group": "6_stash", - "when": "scmProvider == git" - }, - { - "command": "git.stashDrop", - "group": "6_stash", + "submenu": "git.stash", + "group": "2_main@6", "when": "scmProvider == git" }, { "command": "git.showOutput", - "group": "7_repository", + "group": "3_footer", "when": "scmProvider == git" } ], @@ -1307,8 +1207,184 @@ "group": "5_copy@2", "when": "config.git.enabled && !git.missing && timelineItem =~ /git:file:commit\\b/" } + ], + "git.commit": [ + { + "command": "git.commit", + "group": "1_commit@1" + }, + { + "command": "git.commitStaged", + "group": "1_commit@2" + }, + { + "command": "git.commitAll", + "group": "1_commit@3" + }, + { + "command": "git.undoCommit", + "group": "1_commit@4" + }, + { + "command": "git.rebaseAbort", + "group": "1_commit@5" + }, + { + "command": "git.commitStagedAmend", + "group": "2_amend@1" + }, + { + "command": "git.commitAllAmend", + "group": "2_amend@1" + }, + { + "command": "git.commitStagedSigned", + "group": "3_signoff@1" + }, + { + "command": "git.commitAllSigned", + "group": "3_signoff@2" + } + ], + "git.changes": [ + { + "command": "git.stageAll" + }, + { + "command": "git.unstageAll" + }, + { + "command": "git.cleanAll" + } + ], + "git.pullpush": [ + { + "command": "git.sync", + "group": "1_sync" + }, + { + "command": "git.syncRebase", + "when": "gitState == idle", + "group": "1_sync" + }, + { + "command": "git.pull", + "group": "2_pull" + }, + { + "command": "git.pullRebase", + "group": "2_pull" + }, + { + "command": "git.pullFrom", + "group": "2_pull" + }, + { + "command": "git.push", + "group": "3_push" + }, + { + "command": "git.pushForce", + "when": "config.git.allowForcePush", + "group": "3_push" + }, + { + "command": "git.pushTo", + "group": "3_push" + }, + { + "command": "git.pushToForce", + "when": "config.git.allowForcePush", + "group": "3_push" + }, + { + "command": "git.fetch", + "group": "4_fetch" + }, + { + "command": "git.fetchPrune", + "group": "4_fetch" + }, + { + "command": "git.fetchAll", + "group": "4_fetch" + } + ], + "git.branch": [ + { + "command": "git.merge" + }, + { + "command": "git.branch" + }, + { + "command": "git.branchFrom" + }, + { + "command": "git.renameBranch" + }, + { + "command": "git.publish" + } + ], + "git.remotes": [ + { + "command": "git.addRemote" + }, + { + "command": "git.removeRemote" + } + ], + "git.stash": [ + { + "command": "git.stash" + }, + { + "command": "git.stashIncludeUntracked" + }, + { + "command": "git.stashApplyLatest" + }, + { + "command": "git.stashApply" + }, + { + "command": "git.stashPopLatest" + }, + { + "command": "git.stashPop" + }, + { + "command": "git.stashDrop" + } ] }, + "submenus": [ + { + "id": "git.commit", + "label": "%submenu.commit%" + }, + { + "id": "git.changes", + "label": "%submenu.changes%" + }, + { + "id": "git.pullpush", + "label": "%submenu.pullpush%" + }, + { + "id": "git.branch", + "label": "%submenu.branch%" + }, + { + "id": "git.remotes", + "label": "%submenu.remotes%" + }, + { + "id": "git.stash", + "label": "%submenu.stash%" + } + ], "configuration": { "title": "Git", "properties": { @@ -1425,6 +1501,11 @@ "description": "%config.ignoreMissingGitWarning%", "default": false }, + "git.ignoreWindowsGit27Warning": { + "type": "boolean", + "description": "%config.ignoreWindowsGit27Warning%", + "default": false + }, "git.ignoreLimitWarning": { "type": "boolean", "description": "%config.ignoreLimitWarning%", @@ -1801,7 +1882,7 @@ "Ignore", "ignore" ], - "filenames": [ + "extensions": [ ".gitignore_global", ".gitignore" ], diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index be817e22df1..0e8ea1c649e 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -99,6 +99,7 @@ "config.branchWhitespaceChar": "The character to replace whitespace in new branch names.", "config.ignoreLegacyWarning": "Ignores the legacy Git warning.", "config.ignoreMissingGitWarning": "Ignores the warning when Git is missing.", + "config.ignoreWindowsGit27Warning": "Ignores the warning when Git 2.25 - 2.26 is installed on Windows.", "config.ignoreLimitWarning": "Ignores the warning when there are too many changes in a repository.", "config.defaultCloneDirectory": "The default location to clone a git repository.", "config.enableSmartCommit": "Commit all changes when there are no staged changes.", @@ -147,6 +148,14 @@ "config.untrackedChanges.hidden": "Untracked changes are hidden and excluded from several actions.", "config.showCommitInput": "Controls whether to show the commit input in the Git source control panel.", "config.terminalAuthentication": "Controls whether to enable VS Code to be the authentication handler for git processes spawned in the integrated terminal. Note: terminals need to be restarted to pick up a change in this setting.", + "submenu.commit": "Commit", + "submenu.commit.amend": "Amend", + "submenu.commit.signoff": "Sign Off", + "submenu.changes": "Changes", + "submenu.pullpush": "Pull, Push", + "submenu.branch": "Branch", + "submenu.remotes": "Remote", + "submenu.stash": "Stash", "colors.added": "Color for added resources.", "colors.modified": "Color for modified resources.", "colors.deleted": "Color for deleted resources.", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index c9fe2503d22..f618a8669c2 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -543,11 +543,11 @@ export class CommandCenter { const uri = Uri.file(repositoryPath); if (openFolder) { - commands.executeCommand('vscode.openFolder', uri); + commands.executeCommand('vscode.openFolder', uri, { forceReuseWindow: true }); } else if (result === addToWorkspace) { workspace.updateWorkspaceFolders(workspace.workspaceFolders!.length, 0, { uri }); } else if (result === openNewWindow) { - commands.executeCommand('vscode.openFolder', uri, true); + commands.executeCommand('vscode.openFolder', uri, { forceNewWindow: true }); } } catch (err) { if (/already exists and is not an empty directory/.test(err && err.stderr || '')) { diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index c1c78e629bf..8bf85a1b3ee 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -74,7 +74,7 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann new GitTimelineProvider(model) ); - await checkGitVersion(info); + checkGitVersion(info); return model; } @@ -208,14 +208,25 @@ async function checkGitWindows(info: IGit): Promise { return; } + const config = workspace.getConfiguration('git'); + const shouldIgnore = config.get('ignoreWindowsGit27Warning') === true; + + if (shouldIgnore) { + return; + } + const update = localize('updateGit', "Update Git"); + const neverShowAgain = localize('neverShowAgain', "Don't Show Again"); const choice = await window.showWarningMessage( localize('git2526', "There are known issues with the installed Git {0}. Please update to Git >= 2.27 for the git features to work correctly.", info.version), - update + update, + neverShowAgain ); if (choice === update) { commands.executeCommand('vscode.open', Uri.parse('https://git-scm.com/')); + } else if (choice === neverShowAgain) { + await config.update('ignoreWindowsGit27Warning', true, true); } } diff --git a/extensions/git/src/statusbar.ts b/extensions/git/src/statusbar.ts index 760a507a8f9..8a108ae1b45 100644 --- a/extensions/git/src/statusbar.ts +++ b/extensions/git/src/statusbar.ts @@ -61,6 +61,15 @@ class SyncStatusBar { } constructor(private repository: Repository, private remoteSourceProviderRegistry: IRemoteSourceProviderRegistry) { + this._state = { + enabled: true, + isSyncRunning: false, + hasRemotes: false, + HEAD: undefined, + remoteSourceProviders: this.remoteSourceProviderRegistry.getRemoteProviders() + .filter(p => !!p.publishRepository) + }; + repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables); repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables); @@ -70,15 +79,6 @@ class SyncStatusBar { const onEnablementChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.enableStatusBarSync')); onEnablementChange(this.updateEnablement, this, this.disposables); this.updateEnablement(); - - this._state = { - enabled: true, - isSyncRunning: false, - hasRemotes: false, - HEAD: undefined, - remoteSourceProviders: this.remoteSourceProviderRegistry.getRemoteProviders() - .filter(p => !!p.publishRepository) - }; } private updateEnablement(): void { diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index d6e2be8613b..5600b6f0e0c 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -65,27 +65,32 @@ export class GitTimelineProvider implements TimelineProvider { readonly id = 'git-history'; readonly label = localize('git.timeline.source', 'Git History'); - private disposable: Disposable; + private readonly disposable: Disposable; + private providerDisposable: Disposable | undefined; private repo: Repository | undefined; private repoDisposable: Disposable | undefined; private repoStatusDate: Date | undefined; - constructor(private readonly _model: Model) { + constructor(private readonly model: Model) { this.disposable = Disposable.from( - _model.onDidOpenRepository(this.onRepositoriesChanged, this), - workspace.registerTimelineProvider(['file', 'git', 'vscode-remote', 'gitlens-git'], this), + model.onDidOpenRepository(this.onRepositoriesChanged, this), ); + + if (model.repositories.length) { + this.ensureProviderRegistration(); + } } dispose() { + this.providerDisposable?.dispose(); this.disposable.dispose(); } async provideTimeline(uri: Uri, options: TimelineOptions, _token: CancellationToken): Promise { // console.log(`GitTimelineProvider.provideTimeline: uri=${uri} state=${this._model.state}`); - const repo = this._model.getRepository(uri); + const repo = this.model.getRepository(uri); if (!repo) { this.repoDisposable?.dispose(); this.repoStatusDate = undefined; @@ -110,7 +115,7 @@ export class GitTimelineProvider implements TimelineProvider { let limit: number | undefined; if (options.limit !== undefined && typeof options.limit !== 'number') { try { - const result = await this._model.git.exec(repo.root, ['rev-list', '--count', `${options.limit.id}..`, '--', uri.fsPath]); + const result = await this.model.git.exec(repo.root, ['rev-list', '--count', `${options.limit.id}..`, '--', uri.fsPath]); if (!result.exitCode) { // Ask for 2 more (1 for the limit commit and 1 for the next commit) than so we can determine if there are more commits limit = Number(result.stdout) + 2; @@ -203,9 +208,17 @@ export class GitTimelineProvider implements TimelineProvider { }; } + private ensureProviderRegistration() { + if (this.providerDisposable === undefined) { + this.providerDisposable = workspace.registerTimelineProvider(['file', 'git', 'vscode-remote', 'gitlens-git'], this); + } + } + private onRepositoriesChanged(_repo: Repository) { // console.log(`GitTimelineProvider.onRepositoriesChanged`); + this.ensureProviderRegistration(); + // TODO@eamodio: Being naive for now and just always refreshing each time there is a new repository this.fireChanged(); } diff --git a/extensions/github-authentication/.vscodeignore b/extensions/github-authentication/.vscodeignore index ee85b884502..5f3350adfb6 100644 --- a/extensions/github-authentication/.vscodeignore +++ b/extensions/github-authentication/.vscodeignore @@ -1,8 +1,10 @@ +.gitignore src/** !src/common/config.json out/** build/** extension.webpack.config.js +extension-browser.webpack.config.js tsconfig.json yarn.lock README.md diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index 0a4c850d7a3..c4189e6b7eb 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -12,7 +12,8 @@ "Other" ], "activationEvents": [ - "*" + "*", + "onAuthenticationRequest:github" ], "contributes": { "commands": [ diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index 0f9284bbecf..a042ebe2a36 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -9,7 +9,7 @@ import { keychain } from './common/keychain'; import { GitHubServer, NETWORK_ERROR } from './githubServer'; import Logger from './common/logger'; -export const onDidChangeSessions = new vscode.EventEmitter(); +export const onDidChangeSessions = new vscode.EventEmitter(); interface SessionData { id: string; diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index 550e68e37e3..aa7dec8e9b4 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -67,15 +67,26 @@ function parseQuery(uri: vscode.Uri) { export class GitHubServer { private _statusBarItem: vscode.StatusBarItem | undefined; + private isTestEnvironment(url: vscode.Uri): boolean { + return url.authority === 'vscode-web-test-playground.azurewebsites.net' || url.authority.startsWith('localhost:'); + } + public async login(scopes: string): Promise { Logger.info('Logging in...'); this.updateStatusBarItem(true); const state = uuid(); const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate`)); - const uri = vscode.Uri.parse(`https://${AUTH_RELAY_SERVER}/authorize/?callbackUri=${encodeURIComponent(callbackUri.toString())}&scope=${scopes}&state=${state}&responseType=code&authServer=https://github.com`); - await vscode.env.openExternal(uri); + if (this.isTestEnvironment(callbackUri)) { + const token = await vscode.window.showInputBox({ prompt: 'GitHub Personal Access Token', ignoreFocusOut: true }); + if (!token) { throw new Error('Sign in failed: No token provided'); } + this.updateStatusBarItem(false); + return token; + } else { + const uri = vscode.Uri.parse(`https://${AUTH_RELAY_SERVER}/authorize/?callbackUri=${encodeURIComponent(callbackUri.toString())}&scope=${scopes}&state=${state}&responseType=code&authServer=https://github.com`); + await vscode.env.openExternal(uri); + } return Promise.race([ promiseFromEvent(uriHandler.event, exchangeCodeForToken(state)), diff --git a/extensions/github-browser/.gitignore b/extensions/github-browser/.gitignore deleted file mode 100644 index c19bd94aaa7..00000000000 --- a/extensions/github-browser/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -dist -out -node_modules diff --git a/extensions/github-browser/README.md b/extensions/github-browser/README.md deleted file mode 100644 index ef4d6f58e8b..00000000000 --- a/extensions/github-browser/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# GitHub FileSystem for Visual Studio Code - -**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. - -## Features - -This extension provides remote GitHub repository features for VS Code. diff --git a/extensions/github-browser/package.json b/extensions/github-browser/package.json deleted file mode 100644 index 9938e25ed54..00000000000 --- a/extensions/github-browser/package.json +++ /dev/null @@ -1,168 +0,0 @@ -{ - "name": "github-browser", - "displayName": "%displayName%", - "description": "%description%", - "publisher": "vscode", - "version": "0.0.1", - "engines": { - "vscode": "^1.45.0" - }, - "enableProposedApi": true, - "private": true, - "categories": [ - "Other" - ], - "activationEvents": [ - "onFileSystem:codespace", - "onFileSystem:github", - "onCommand:githubBrowser.openRepository" - ], - "browser": "./dist/browser/extension.js", - "main": "./out/extension.js", - "contributes": { - "commands": [ - { - "command": "githubBrowser.openRepository", - "title": "Open GitHub Repository...", - "category": "GitHub Browser" - }, - { - "command": "githubBrowser.commit", - "title": "Commit", - "icon": "$(check)", - "category": "GitHub Browser" - }, - { - "command": "githubBrowser.discardChanges", - "title": "Discard Changes", - "icon": "$(discard)", - "category": "GitHub Browser" - }, - { - "command": "githubBrowser.openChanges", - "title": "Open Changes", - "icon": "$(git-compare)", - "category": "GitHub Browser" - }, - { - "command": "githubBrowser.openFile", - "title": "Open File", - "icon": "$(go-to-file)", - "category": "GitHub Browser" - } - ], - "menus": { - "commandPalette": [ - { - "command": "githubBrowser.openRepository", - "when": "config.githubBrowser.openRepository" - }, - { - "command": "githubBrowser.commit", - "when": "false" - }, - { - "command": "githubBrowser.discardChanges", - "when": "false" - }, - { - "command": "githubBrowser.openChanges", - "when": "false" - }, - { - "command": "githubBrowser.openFile", - "when": "false" - } - ], - "scm/title": [ - { - "command": "githubBrowser.commit", - "group": "navigation", - "when": "scmProvider == github" - } - ], - "scm/resourceState/context": [ - { - "command": "githubBrowser.openFile", - "when": "scmProvider == github && scmResourceGroup == github.changes", - "group": "inline@0" - }, - { - "command": "githubBrowser.discardChanges", - "when": "scmProvider == github && scmResourceGroup == github.changes", - "group": "inline@1" - }, - { - "command": "githubBrowser.openChanges", - "when": "scmProvider == github && scmResourceGroup == github.changes", - "group": "navigation@0" - }, - { - "command": "githubBrowser.openFile", - "when": "scmProvider == github && scmResourceGroup == github.changes", - "group": "navigation@1" - }, - { - "command": "githubBrowser.discardChanges", - "when": "scmProvider == github && scmResourceGroup == github.changes", - "group": "1_modification@0" - } - ] - }, - "resourceLabelFormatters": [ - { - "scheme": "github", - "authority": "HEAD", - "formatting": { - "label": "github.com${path}", - "separator": "/", - "workspaceSuffix": "GitHub" - } - }, - { - "scheme": "github", - "authority": "*", - "formatting": { - "label": "github.com${path} (${authority})", - "separator": "/", - "workspaceSuffix": "GitHub" - } - }, - { - "scheme": "codespace", - "authority": "HEAD", - "formatting": { - "label": "github.com${path}", - "separator": "/", - "workspaceSuffix": "GitHub" - } - }, - { - "scheme": "codespace", - "authority": "*", - "formatting": { - "label": "github.com${path} (${authority})", - "separator": "/", - "workspaceSuffix": "GitHub" - } - } - ] - }, - "scripts": { - "compile": "gulp compile-extension:github-browser", - "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", - "watch": "gulp watch-extension:github-browser", - "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose", - "vscode:prepublish": "npm run compile" - }, - "dependencies": { - "@octokit/graphql": "4.5.1", - "@octokit/rest": "18.0.0", - "fuzzysort": "1.1.4", - "node-fetch": "2.6.0", - "vscode-nls": "4.1.2" - }, - "devDependencies": { - "@types/node-fetch": "2.5.7" - } -} diff --git a/extensions/github-browser/package.nls.json b/extensions/github-browser/package.nls.json deleted file mode 100644 index 69f0f911776..00000000000 --- a/extensions/github-browser/package.nls.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "displayName": "GitHub Browser", - "description": "Remotely browse a GitHub repository" -} diff --git a/extensions/github-browser/src/changeStore.ts b/extensions/github-browser/src/changeStore.ts deleted file mode 100644 index f4bd624b9e7..00000000000 --- a/extensions/github-browser/src/changeStore.ts +++ /dev/null @@ -1,380 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; -import { commands, Event, EventEmitter, FileStat, FileType, Memento, TextDocumentShowOptions, Uri, ViewColumn } from 'vscode'; -import { getRootUri, getRelativePath, isChild } from './extension'; -import { sha1 } from './sha1'; - -const textDecoder = new TextDecoder(); - -interface CreateOperation { - type: 'created'; - size: number; - timestamp: number; - uri: T; - hash: string; - originalHash: string; -} - -interface ChangeOperation { - type: 'changed'; - size: number; - timestamp: number; - uri: T; - hash: string; - originalHash: string; -} - -interface DeleteOperation { - type: 'deleted'; - size: undefined; - timestamp: number; - uri: T; - hash: undefined; - originalHash: undefined; -} - -export type Operation = CreateOperation | ChangeOperation | DeleteOperation; -type StoredOperation = CreateOperation | ChangeOperation | DeleteOperation; - -const workingOperationsKeyPrefix = 'github.working.changes|'; -const workingFileKeyPrefix = 'github.working|'; - -function fromSerialized(operations: StoredOperation): Operation { - return { ...operations, uri: Uri.parse(operations.uri) }; -} - -export interface ChangeStoreEvent { - type: 'created' | 'changed' | 'deleted'; - rootUri: Uri; - uri: Uri; -} - -function toChangeStoreEvent(operation: Operation | StoredOperation, rootUri: Uri, uri?: Uri): ChangeStoreEvent { - return { - type: operation.type, - rootUri: rootUri, - uri: uri ?? (typeof operation.uri === 'string' ? Uri.parse(operation.uri) : operation.uri), - }; -} - -export interface IChangeStore { - onDidChange: Event; - - acceptAll(rootUri: Uri): Promise; - discard(uri: Uri): Promise; - discardAll(rootUri: Uri): Promise; - - hasChanges(rootUri: Uri): boolean; - - getChanges(rootUri: Uri): Operation[]; - getContent(uri: Uri): string | undefined; - - openChanges(uri: Uri, original: Uri): void; - openFile(uri: Uri): void; -} - -export interface IWritableChangeStore { - onDidChange: Event; - - hasChanges(rootUri: Uri): boolean; - - getContent(uri: Uri): string | undefined; - getStat(uri: Uri): FileStat | undefined; - updateDirectoryEntries(uri: Uri, entries: [string, FileType][]): [string, FileType][]; - - onFileChanged(uri: Uri, content: Uint8Array, originalContent: () => Uint8Array | Thenable): Promise; - onFileCreated(uri: Uri, content: Uint8Array): Promise; - onFileDeleted(uri: Uri): Promise; -} - -export class ChangeStore implements IChangeStore, IWritableChangeStore { - private _onDidChange = new EventEmitter(); - get onDidChange(): Event { - return this._onDidChange.event; - } - - constructor(private readonly memento: Memento) { } - - async acceptAll(rootUri: Uri): Promise { - const operations = this.getChanges(rootUri); - - await this.saveWorkingOperations(rootUri, undefined); - - const events: ChangeStoreEvent[] = []; - - for (const operation of operations) { - await this.discardWorkingContent(operation.uri); - events.push(toChangeStoreEvent(operation, rootUri)); - } - - for (const e of events) { - this._onDidChange.fire(e); - } - } - - async discard(uri: Uri): Promise { - const rootUri = getRootUri(uri); - if (rootUri === undefined) { - return; - } - - const key = uri.toString(); - - const operations = this.getWorkingOperations(rootUri); - const index = operations.findIndex(c => c.uri === key); - if (index === -1) { - return; - } - - const [operation] = operations.splice(index, 1); - await this.saveWorkingOperations(rootUri, operations); - await this.discardWorkingContent(uri); - - this._onDidChange.fire({ - type: operation.type === 'created' ? 'deleted' : operation.type === 'deleted' ? 'created' : 'changed', - rootUri: rootUri, - uri: uri, - }); - } - - async discardAll(rootUri: Uri): Promise { - const operations = this.getChanges(rootUri); - - await this.saveWorkingOperations(rootUri, undefined); - - const events: ChangeStoreEvent[] = []; - - for (const operation of operations) { - await this.discardWorkingContent(operation.uri); - events.push(toChangeStoreEvent(operation, rootUri)); - } - - for (const e of events) { - this._onDidChange.fire(e); - } - } - - getChanges(rootUri: Uri) { - return this.getWorkingOperations(rootUri).map(c => fromSerialized(c)); - } - - getContent(uri: Uri): string | undefined { - return this.memento.get(`${workingFileKeyPrefix}${uri.toString()}`); - } - - getStat(uri: Uri): FileStat | undefined { - const key = uri.toString(); - const operation = this.getChanges(getRootUri(uri)!).find(c => c.uri.toString() === key); - if (operation === undefined) { - return undefined; - } - - return { - type: FileType.File, - size: operation.size ?? 0, - ctime: 0, - mtime: operation.timestamp - }; - } - - hasChanges(rootUri: Uri): boolean { - return this.getWorkingOperations(rootUri).length !== 0; - } - - updateDirectoryEntries(uri: Uri, entries: [string, FileType][]): [string, FileType][] { - const rootUri = getRootUri(uri); - if (rootUri === undefined) { - return entries; - } - - const folderPath = getRelativePath(rootUri, uri); - - const operations = this.getChanges(rootUri); - for (const operation of operations) { - switch (operation.type) { - case 'changed': - continue; - - case 'created': { - const filePath = getRelativePath(rootUri, operation.uri); - if (isChild(folderPath, filePath)) { - entries.push([filePath, FileType.File]); - } - break; - } - - case 'deleted': { - const filePath = getRelativePath(rootUri, operation.uri); - if (isChild(folderPath, filePath)) { - const index = entries.findIndex(([path]) => path === filePath); - if (index !== -1) { - entries.splice(index, 1); - } - } - break; - } - } - } - - return entries; - } - - async onFileChanged(uri: Uri, content: Uint8Array, originalContent: () => Uint8Array | Thenable): Promise { - const rootUri = getRootUri(uri); - if (rootUri === undefined) { - return; - } - - const key = uri.toString(); - - const operations = this.getWorkingOperations(rootUri); - - const hash = await sha1(content); - - let operation = operations.find(c => c.uri === key); - if (operation === undefined) { - const originalHash = await sha1(await originalContent!()); - if (hash === originalHash) { - return; - } - - operation = { - type: 'changed', - size: content.byteLength, - timestamp: Date.now(), - uri: key, - hash: hash!, - originalHash: originalHash - } as ChangeOperation; - operations.push(operation); - - await this.saveWorkingOperations(rootUri, operations); - await this.saveWorkingContent(uri, textDecoder.decode(content)); - } else if (hash! === operation.originalHash) { - operations.splice(operations.indexOf(operation), 1); - - await this.saveWorkingOperations(rootUri, operations); - await this.discardWorkingContent(uri); - } else if (operation.hash !== hash) { - operation.hash = hash!; - operation.timestamp = Date.now(); - - await this.saveWorkingOperations(rootUri, operations); - await this.saveWorkingContent(uri, textDecoder.decode(content)); - } - - this._onDidChange.fire(toChangeStoreEvent(operation, rootUri, uri)); - } - - async onFileCreated(uri: Uri, content: Uint8Array): Promise { - const rootUri = getRootUri(uri); - if (rootUri === undefined) { - return; - } - - const key = uri.toString(); - - const operations = this.getWorkingOperations(rootUri); - - const hash = await sha1(content); - - let operation = operations.find(c => c.uri === key); - if (operation === undefined) { - operation = { - type: 'created', - size: content.byteLength, - timestamp: Date.now(), - uri: key, - hash: hash!, - originalHash: hash! - } as CreateOperation; - operations.push(operation); - - await this.saveWorkingOperations(rootUri, operations); - await this.saveWorkingContent(uri, textDecoder.decode(content)); - } else { - // Shouldn't happen, but if it does just update the contents - operation.hash = hash!; - operation.timestamp = Date.now(); - - await this.saveWorkingOperations(rootUri, operations); - await this.saveWorkingContent(uri, textDecoder.decode(content)); - } - - this._onDidChange.fire(toChangeStoreEvent(operation, rootUri, uri)); - } - - async onFileDeleted(uri: Uri): Promise { - const rootUri = getRootUri(uri); - if (rootUri === undefined) { - return; - } - - const key = uri.toString(); - - const operations = this.getWorkingOperations(rootUri); - - let operation = operations.find(c => c.uri === key); - if (operation !== undefined) { - operations.splice(operations.indexOf(operation), 1); - } - - const wasCreated = operation?.type === 'created'; - - operation = { - type: 'deleted', - timestamp: Date.now(), - uri: key, - } as DeleteOperation; - - // Only track the delete, if we weren't tracking the create - if (!wasCreated) { - operations.push(operation); - } - - await this.saveWorkingOperations(rootUri, operations); - await this.discardWorkingContent(uri); - - this._onDidChange.fire(toChangeStoreEvent(operation, rootUri, uri)); - } - - async openChanges(uri: Uri, original: Uri) { - const opts: TextDocumentShowOptions = { - preserveFocus: false, - preview: true, - viewColumn: ViewColumn.Active - }; - - await commands.executeCommand('vscode.diff', original, uri, `${uri.fsPath} (Working Tree)`, opts); - } - - async openFile(uri: Uri) { - const opts: TextDocumentShowOptions = { - preserveFocus: false, - preview: false, - viewColumn: ViewColumn.Active - }; - - await commands.executeCommand('vscode.open', uri, opts); - } - - private getWorkingOperations(rootUri: Uri): StoredOperation[] { - return this.memento.get(`${workingOperationsKeyPrefix}${rootUri.toString()}`, []); - } - - private async saveWorkingOperations(rootUri: Uri, operations: StoredOperation[] | undefined): Promise { - await this.memento.update(`${workingOperationsKeyPrefix}${rootUri.toString()}`, operations); - } - - private async saveWorkingContent(uri: Uri, content: string): Promise { - await this.memento.update(`${workingFileKeyPrefix}${uri.toString()}`, content); - } - - private async discardWorkingContent(uri: Uri): Promise { - await this.memento.update(`${workingFileKeyPrefix}${uri.toString()}`, undefined); - } -} diff --git a/extensions/github-browser/src/contextStore.ts b/extensions/github-browser/src/contextStore.ts deleted file mode 100644 index 80286445dfe..00000000000 --- a/extensions/github-browser/src/contextStore.ts +++ /dev/null @@ -1,53 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; -import { Event, EventEmitter, Memento, Uri, workspace } from 'vscode'; - -export interface WorkspaceFolderContext { - context: T; - name: string; - folderUri: Uri; -} - -export class ContextStore { - private _onDidChange = new EventEmitter(); - get onDidChange(): Event { - return this._onDidChange.event; - } - - constructor( - private readonly scheme: string, - private readonly originalScheme: string, - private readonly memento: Memento, - ) { } - - delete(uri: Uri) { - return this.set(uri, undefined); - } - - get(uri: Uri): T | undefined { - return this.memento.get(`${this.originalScheme}.context|${this.getOriginalResource(uri).toString()}`); - } - - getForWorkspace(): WorkspaceFolderContext[] { - const folders = workspace.workspaceFolders?.filter(f => f.uri.scheme === this.scheme || f.uri.scheme === this.originalScheme) ?? []; - return folders.map(f => ({ context: this.get(f.uri)!, name: f.name, folderUri: f.uri })).filter(c => c.context !== undefined); - } - - async set(uri: Uri, context: T | undefined) { - uri = this.getOriginalResource(uri); - await this.memento.update(`${this.originalScheme}.context|${uri.toString()}`, context); - this._onDidChange.fire(uri); - } - - getOriginalResource(uri: Uri): Uri { - return uri.with({ scheme: this.originalScheme }); - } - - getWorkspaceResource(uri: Uri): Uri { - return uri.with({ scheme: this.scheme }); - } -} diff --git a/extensions/github-browser/src/extension.ts b/extensions/github-browser/src/extension.ts deleted file mode 100644 index 893daf93c58..00000000000 --- a/extensions/github-browser/src/extension.ts +++ /dev/null @@ -1,80 +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 { commands, ExtensionContext, Uri, window, workspace } from 'vscode'; -import { ChangeStore } from './changeStore'; -import { ContextStore } from './contextStore'; -import { VirtualFS } from './fs'; -import { GitHubApiContext, GitHubApi } from './github/api'; -import { GitHubFS } from './github/fs'; -import { VirtualSCM } from './scm'; -import { StatusBar } from './statusbar'; - -const repositoryRegex = /^(?:(?:https:\/\/)?github.com\/)?([^\/]+)\/([^\/]+?)(?:\/|.git|$)/i; - -export async function activate(context: ExtensionContext) { - const contextStore = new ContextStore('codespace', GitHubFS.scheme, context.workspaceState); - const changeStore = new ChangeStore(context.workspaceState); - - const githubApi = new GitHubApi(contextStore); - const gitHubFS = new GitHubFS(githubApi); - const virtualFS = new VirtualFS('codespace', contextStore, changeStore, gitHubFS); - - context.subscriptions.push( - githubApi, - gitHubFS, - virtualFS, - new VirtualSCM(GitHubFS.scheme, githubApi, changeStore), - new StatusBar(contextStore, changeStore), - ); - - commands.registerCommand('githubBrowser.openRepository', async () => { - const value = await window.showInputBox({ - placeHolder: 'e.g. https://github.com/microsoft/vscode', - prompt: 'Enter a GitHub repository url', - validateInput: value => repositoryRegex.test(value) ? undefined : 'Invalid repository url' - }); - - if (value) { - const match = repositoryRegex.exec(value); - if (match) { - const [, owner, repo] = match; - - const uri = Uri.parse(`codespace://HEAD/${owner}/${repo}`); - openWorkspace(uri, repo, 'currentWindow'); - } - } - }); -} - -export function getRelativePath(rootUri: Uri, uri: Uri) { - return uri.path.substr(rootUri.path.length + 1); -} - -export function getRootUri(uri: Uri) { - return workspace.getWorkspaceFolder(uri)?.uri; -} - -export function isChild(folderPath: string, filePath: string) { - return isDescendent(folderPath, filePath) && filePath.substr(folderPath.length + (folderPath.endsWith('/') ? 0 : 1)).split('/').length === 1; -} - -export function isDescendent(folderPath: string, filePath: string) { - return folderPath.length === 0 || filePath.startsWith(folderPath.endsWith('/') ? folderPath : `${folderPath}/`); -} - -const shaRegex = /^[0-9a-f]{40}$/; -export function isSha(ref: string) { - return shaRegex.test(ref); -} - -function openWorkspace(uri: Uri, name: string, location: 'currentWindow' | 'newWindow' | 'addToCurrentWorkspace') { - if (location === 'addToCurrentWorkspace') { - const count = (workspace.workspaceFolders && workspace.workspaceFolders.length) || 0; - return workspace.updateWorkspaceFolders(count, 0, { uri: uri, name: name }); - } - - return commands.executeCommand('vscode.openFolder', uri, location === 'newWindow'); -} diff --git a/extensions/github-browser/src/fs.ts b/extensions/github-browser/src/fs.ts deleted file mode 100644 index 56af40f21ba..00000000000 --- a/extensions/github-browser/src/fs.ts +++ /dev/null @@ -1,216 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; -import { - CancellationToken, - Disposable, - Event, - EventEmitter, - FileChangeEvent, - FileChangeType, - FileSearchOptions, - FileSearchProvider, - FileSearchQuery, - FileStat, - FileSystemError, - FileSystemProvider, - FileType, - Progress, - TextSearchOptions, - TextSearchProvider, - TextSearchQuery, - TextSearchResult, - Uri, - workspace, -} from 'vscode'; -import { IWritableChangeStore } from './changeStore'; -import { ContextStore } from './contextStore'; -import { GitHubApiContext } from './github/api'; - -const emptyDisposable = { dispose: () => { /* noop */ } }; -const textEncoder = new TextEncoder(); - -export class VirtualFS implements FileSystemProvider, FileSearchProvider, TextSearchProvider, Disposable { - private _onDidChangeFile = new EventEmitter(); - get onDidChangeFile(): Event { - return this._onDidChangeFile.event; - } - - private readonly disposable: Disposable; - - constructor( - readonly scheme: string, - private readonly contextStore: ContextStore, - private readonly changeStore: IWritableChangeStore, - private readonly fs: FileSystemProvider & FileSearchProvider & TextSearchProvider - ) { - // TODO@eamodio listen for workspace folder changes - for (const context of contextStore.getForWorkspace()) { - // If we have a saved context, but no longer have any changes, reset the context - // We only do this on startup/reload to keep things consistent - if (!changeStore.hasChanges(context.folderUri)) { - console.log('Clear context', context.folderUri.toString()); - contextStore.delete(context.folderUri); - } - } - - this.disposable = Disposable.from( - workspace.registerFileSystemProvider(scheme, this, { isCaseSensitive: true }), - workspace.registerFileSearchProvider(scheme, this), - workspace.registerTextSearchProvider(scheme, this), - changeStore.onDidChange(e => { - switch (e.type) { - case 'created': - this._onDidChangeFile.fire([{ type: FileChangeType.Created, uri: e.uri }]); - break; - case 'changed': - this._onDidChangeFile.fire([{ type: FileChangeType.Changed, uri: e.uri }]); - break; - case 'deleted': - this._onDidChangeFile.fire([{ type: FileChangeType.Deleted, uri: e.uri }]); - break; - } - }), - ); - } - - dispose() { - this.disposable?.dispose(); - } - - private getOriginalResource(uri: Uri): Uri { - return this.contextStore.getOriginalResource(uri); - } - - private getWorkspaceResource(uri: Uri): Uri { - return this.contextStore.getWorkspaceResource(uri); - } - - //#region FileSystemProvider - - watch(): Disposable { - return emptyDisposable; - } - - async stat(uri: Uri): Promise { - let stat = this.changeStore.getStat(uri); - if (stat !== undefined) { - return stat; - } - - stat = await this.fs.stat(this.getOriginalResource(uri)); - return stat; - } - - async readDirectory(uri: Uri): Promise<[string, FileType][]> { - let entries = await this.fs.readDirectory(this.getOriginalResource(uri)); - entries = this.changeStore.updateDirectoryEntries(uri, entries); - return entries; - } - - createDirectory(_uri: Uri): void | Thenable { - // TODO@eamodio only support files for now - throw FileSystemError.NoPermissions(); - } - - async readFile(uri: Uri): Promise { - const content = this.changeStore.getContent(uri); - if (content !== undefined) { - return textEncoder.encode(content); - } - - const data = await this.fs.readFile(this.getOriginalResource(uri)); - return data; - } - - async writeFile(uri: Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): Promise { - let stat; - try { - stat = await this.stat(uri); - if (!options.overwrite) { - throw FileSystemError.FileExists(); - } - } catch (ex) { - if (ex instanceof FileSystemError && ex.code === 'FileNotFound') { - if (!options.create) { - throw FileSystemError.FileNotFound(); - } - } else { - throw ex; - } - } - - if (stat === undefined) { - await this.changeStore.onFileCreated(uri, content); - } else { - await this.changeStore.onFileChanged(uri, content, () => this.fs.readFile(this.getOriginalResource(uri))); - } - } - - async delete(uri: Uri, _options: { recursive: boolean }): Promise { - const stat = await this.stat(uri); - if (stat.type !== FileType.File) { - throw FileSystemError.NoPermissions(); - } - - await this.changeStore.onFileDeleted(uri); - } - - async rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean }): Promise { - const stat = await this.stat(oldUri); - // TODO@eamodio only support files for now - if (stat.type !== FileType.File) { - throw FileSystemError.NoPermissions(); - } - - const content = await this.readFile(oldUri); - await this.writeFile(newUri, content, { create: true, overwrite: options.overwrite }); - await this.delete(oldUri, { recursive: false }); - } - - async copy(source: Uri, destination: Uri, options: { overwrite: boolean }): Promise { - const stat = await this.stat(source); - // TODO@eamodio only support files for now - if (stat.type !== FileType.File) { - throw FileSystemError.NoPermissions(); - } - - const content = await this.readFile(source); - await this.writeFile(destination, content, { create: true, overwrite: options.overwrite }); - } - - //#endregion - - //#region FileSearchProvider - - provideFileSearchResults( - query: FileSearchQuery, - options: FileSearchOptions, - token: CancellationToken, - ) { - return this.fs.provideFileSearchResults(query, { ...options, folder: this.getOriginalResource(options.folder) }, token); - } - - //#endregion - - //#region TextSearchProvider - - provideTextSearchResults( - query: TextSearchQuery, - options: TextSearchOptions, - progress: Progress, - token: CancellationToken, - ) { - return this.fs.provideTextSearchResults( - query, - { ...options, folder: this.getOriginalResource(options.folder) }, - { report: (result: TextSearchResult) => progress.report({ ...result, uri: this.getWorkspaceResource(result.uri) }) }, - token - ); - } - - //#endregion -} diff --git a/extensions/github-browser/src/gate.ts b/extensions/github-browser/src/gate.ts deleted file mode 100644 index d49762dc748..00000000000 --- a/extensions/github-browser/src/gate.ts +++ /dev/null @@ -1,87 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -const emptyStr = ''; - -function defaultResolver(...args: any[]): string { - if (args.length === 1) { - const arg0 = args[0]; - if (arg0 === undefined || arg0 === null) { - return emptyStr; - } - if (typeof arg0 === 'string') { - return arg0; - } - if (typeof arg0 === 'number' || typeof arg0 === 'boolean') { - return String(arg0); - } - - return JSON.stringify(arg0); - } - - return JSON.stringify(args); -} - -function iPromise(obj: T | Promise): obj is Promise { - return typeof (obj as Promise)?.then === 'function'; -} - -export function gate any>(resolver?: (...args: Parameters) => string) { - return (_target: any, key: string, descriptor: PropertyDescriptor) => { - let fn: Function | undefined; - if (typeof descriptor.value === 'function') { - fn = descriptor.value; - } else if (typeof descriptor.get === 'function') { - fn = descriptor.get; - } - if (fn === undefined || fn === null) { - throw new Error('Not supported'); - } - - const gateKey = `$gate$${key}`; - - descriptor.value = function (this: any, ...args: any[]) { - const prop = - args.length === 0 ? gateKey : `${gateKey}$${(resolver ?? defaultResolver)(...(args as Parameters))}`; - - if (!Object.prototype.hasOwnProperty.call(this, prop)) { - Object.defineProperty(this, prop, { - configurable: false, - enumerable: false, - writable: true, - value: undefined, - }); - } - - let promise = this[prop]; - if (promise === undefined) { - let result; - try { - result = fn!.apply(this, args); - if (result === undefined || fn === null || !iPromise(result)) { - return result; - } - - this[prop] = promise = result - .then((r: any) => { - this[prop] = undefined; - return r; - }) - .catch(ex => { - this[prop] = undefined; - throw ex; - }); - } catch (ex) { - this[prop] = undefined; - throw ex; - } - } - - return promise; - }; - }; -} diff --git a/extensions/github-browser/src/github/api.ts b/extensions/github-browser/src/github/api.ts deleted file mode 100644 index bb2bb65cb6d..00000000000 --- a/extensions/github-browser/src/github/api.ts +++ /dev/null @@ -1,504 +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 { authentication, AuthenticationSession, Disposable, Event, EventEmitter, Range, Uri } from 'vscode'; -import { graphql } from '@octokit/graphql'; -import { Octokit } from '@octokit/rest'; -import { ContextStore } from '../contextStore'; -import { fromGitHubUri } from './fs'; -import { isSha } from '../extension'; -import { Iterables } from '../iterables'; - -export interface GitHubApiContext { - requestRef: string; - - branch: string; - sha: string | undefined; - timestamp: number; -} - -interface CreateCommitOperation { - type: 'created'; - path: string; - content: string -} - -interface ChangeCommitOperation { - type: 'changed'; - path: string; - content: string -} - -interface DeleteCommitOperation { - type: 'deleted'; - path: string; - content: undefined -} - -export type CommitOperation = CreateCommitOperation | ChangeCommitOperation | DeleteCommitOperation; - -type ArrayElement> = T extends (infer U)[] ? U : never; -type GitCreateTreeParamsTree = ArrayElement[0]>['tree']>; - -function getGitHubRootUri(uri: Uri) { - const rootIndex = uri.path.indexOf('/', uri.path.indexOf('/', 1) + 1); - return uri.with({ - path: uri.path.substring(0, rootIndex === -1 ? undefined : rootIndex), - query: '' - }); -} - -export class GitHubApi implements Disposable { - private _onDidChangeContext = new EventEmitter(); - get onDidChangeContext(): Event { - return this._onDidChangeContext.event; - } - - private readonly disposable: Disposable; - - constructor(private readonly context: ContextStore) { - this.disposable = Disposable.from( - context.onDidChange(e => this._onDidChangeContext.fire(e)) - ); - } - - dispose() { - this.disposable.dispose(); - } - - private _session: AuthenticationSession | undefined; - async ensureAuthenticated() { - if (this._session === undefined) { - const providers = await authentication.getProviderIds(); - if (!providers.includes('github')) { - await new Promise(resolve => { - authentication.onDidChangeAuthenticationProviders(e => { - if (e.added.find(provider => provider.id === 'github')) { - resolve(); - } - }); - }); - } - - this._session = await authentication.getSession('github', ['repo'], { createIfNone: true }); - } - - return this._session; - } - - private _graphql: typeof graphql | undefined; - private async graphql() { - if (this._graphql === undefined) { - const session = await this.ensureAuthenticated(); - this._graphql = graphql.defaults({ - headers: { - Authorization: `Bearer ${session.accessToken}`, - } - }); - } - - return this._graphql; - } - - private _octokit: typeof Octokit | undefined; - private async octokit(options?: ConstructorParameters[0]) { - if (this._octokit === undefined) { - const session = await this.ensureAuthenticated(); - this._octokit = Octokit.defaults({ auth: `token ${session.accessToken}` }); - } - return new this._octokit(options); - } - - async commit(rootUri: Uri, message: string, operations: CommitOperation[]): Promise { - const { owner, repo } = fromGitHubUri(rootUri); - - try { - const context = await this.getContext(rootUri); - if (context.sha === undefined) { - throw new Error(`Cannot commit to Uri(${rootUri.toString(true)}); Invalid context sha`); - } - - const hasDeletes = operations.some(op => op.type === 'deleted'); - - const github = await this.octokit(); - const treeResp = await github.git.getTree({ - owner: owner, - repo: repo, - tree_sha: context.sha, - recursive: hasDeletes ? 'true' : undefined, - }); - - // 0100000000000000 (040000): Directory - // 1000000110100100 (100644): Regular non-executable file - // 1000000110110100 (100664): Regular non-executable group-writeable file - // 1000000111101101 (100755): Regular executable file - // 1010000000000000 (120000): Symbolic link - // 1110000000000000 (160000): Gitlink - let updatedTree: GitCreateTreeParamsTree[]; - - if (hasDeletes) { - updatedTree = treeResp.data.tree as GitCreateTreeParamsTree[]; - - for (const operation of operations) { - switch (operation.type) { - case 'created': - updatedTree.push({ path: operation.path, mode: '100644', type: 'blob', content: operation.content }); - break; - - case 'changed': { - const index = updatedTree.findIndex(item => item.path === operation.path); - if (index !== -1) { - const { path, mode, type } = updatedTree[index]; - updatedTree.splice(index, 1, { path: path, mode: mode, type: type, content: operation.content }); - } - break; - } - case 'deleted': { - const index = updatedTree.findIndex(item => item.path === operation.path); - if (index !== -1) { - updatedTree.splice(index, 1); - } - break; - } - } - } - } else { - updatedTree = []; - - for (const operation of operations) { - switch (operation.type) { - case 'created': - updatedTree.push({ path: operation.path, mode: '100644', type: 'blob', content: operation.content }); - break; - - case 'changed': - const item = treeResp.data.tree.find(item => item.path === operation.path) as GitCreateTreeParamsTree; - if (item !== undefined) { - const { path, mode, type } = item; - updatedTree.push({ path: path, mode: mode, type: type, content: operation.content }); - } - break; - } - } - } - - const updatedTreeResp = await github.git.createTree({ - owner: owner, - repo: repo, - base_tree: hasDeletes ? undefined : treeResp.data.sha, - tree: updatedTree - }); - - const resp = await github.git.createCommit({ - owner: owner, - repo: repo, - message: message, - tree: updatedTreeResp.data.sha, - parents: [context.sha] - }); - - this.updateContext(rootUri, { ...context, sha: resp.data.sha, timestamp: Date.now() }); - - // TODO@eamodio need to send a file change for any open files - - await github.git.updateRef({ - owner: owner, - repo: repo, - ref: `heads/${context.branch}`, - sha: resp.data.sha - }); - - return resp.data.sha; - } catch (ex) { - console.log(ex); - throw ex; - } - } - - async defaultBranchQuery(uri: Uri) { - const { owner, repo } = fromGitHubUri(uri); - - try { - const query = `query defaultBranch($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - defaultBranchRef { - name - } - } -}`; - - const rsp = await this.gqlQuery<{ - repository: { defaultBranchRef: { name: string; target: { oid: string } } | null | undefined }; - }>(query, { - owner: owner, - repo: repo, - }); - return rsp?.repository?.defaultBranchRef?.name ?? undefined; - } catch (ex) { - return undefined; - } - } - - async filesQuery(uri: Uri) { - const { owner, repo, ref } = fromGitHubUri(uri); - - try { - const context = await this.getContext(uri); - - const resp = await (await this.octokit()).git.getTree({ - owner: owner, - repo: repo, - recursive: '1', - tree_sha: context?.sha ?? ref, - }); - return Iterables.filterMap(resp.data.tree, p => p.type === 'blob' ? p.path : undefined); - } catch (ex) { - return []; - } - } - - async fsQuery(uri: Uri, innerQuery: string): Promise { - const { owner, repo, path, ref } = fromGitHubUri(uri); - - try { - const context = await this.getContext(uri); - - const query = `query fs($owner: String!, $repo: String!, $path: String) { - repository(owner: $owner, name: $repo) { - object(expression: $path) { - ${innerQuery} - } - } -}`; - - const rsp = await this.gqlQuery<{ - repository: { object: T | null | undefined }; - }>(query, { - owner: owner, - repo: repo, - path: `${context.sha ?? ref}:${path}`, - }); - return rsp?.repository?.object ?? undefined; - } catch (ex) { - return undefined; - } - } - - async latestCommitQuery(uri: Uri) { - const { owner, repo, ref } = fromGitHubUri(uri); - - try { - if (ref === 'HEAD') { - const query = `query latest($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - defaultBranchRef { - target { - oid - } - } - } -}`; - - const rsp = await this.gqlQuery<{ - repository: { defaultBranchRef: { name: string; target: { oid: string } } | null | undefined }; - }>(query, { - owner: owner, - repo: repo, - }); - return rsp?.repository?.defaultBranchRef?.target.oid ?? undefined; - } - - const query = `query latest($owner: String!, $repo: String!, $ref: String!) { - repository(owner: $owner, name: $repo) { - ref(qualifiedName: $ref) { - target { - oid - } - } - } -}`; - - const rsp = await this.gqlQuery<{ - repository: { ref: { target: { oid: string } } | null | undefined }; - }>(query, { - owner: owner, - repo: repo, - ref: ref ?? 'HEAD', - }); - return rsp?.repository?.ref?.target.oid ?? undefined; - } catch (ex) { - return undefined; - } - } - - async searchQuery( - query: string, - uri: Uri, - options: { maxResults?: number; context?: { before?: number; after?: number } }, - ): Promise { - const { owner, repo, ref } = fromGitHubUri(uri); - - // If we have a specific ref, don't try to search, because GitHub search only works against the default branch - if (ref !== 'HEAD') { - return { matches: [], limitHit: true }; - } - - try { - const resp = await (await this.octokit({ - request: { - headers: { - accept: 'application/vnd.github.v3.text-match+json', - }, - } - })).search.code({ - q: `${query} repo:${owner}/${repo}`, - }); - - // Since GitHub doesn't return ANY line numbers just fake it at the top of the file 😢 - const range = new Range(0, 0, 0, 0); - - const matches: SearchQueryMatch[] = []; - - let counter = 0; - let match: SearchQueryMatch; - for (const item of resp.data.items) { - for (const m of (item as typeof item & { text_matches: GitHubSearchTextMatch[] }).text_matches) { - counter++; - if (options.maxResults !== undefined && counter > options.maxResults) { - return { matches: matches, limitHit: true }; - } - - match = { - path: item.path, - ranges: [], - preview: m.fragment, - matches: [], - }; - - for (const lm of m.matches) { - let line = 0; - let shartChar = 0; - let endChar = 0; - for (let i = 0; i < lm.indices[1]; i++) { - if (i === lm.indices[0]) { - shartChar = endChar; - } - - if (m.fragment[i] === '\n') { - line++; - endChar = 0; - } else { - endChar++; - } - } - - match.ranges.push(range); - match.matches.push(new Range(line, shartChar, line, endChar)); - } - - matches.push(match); - } - } - - return { matches: matches, limitHit: false }; - } catch (ex) { - return { matches: [], limitHit: true }; - } - } - - private async gqlQuery(query: string, variables: { [key: string]: string | number }): Promise { - return (await this.graphql())(query, variables); - } - - private readonly pendingContextRequests = new Map>(); - async getContext(uri: Uri): Promise { - const rootUri = getGitHubRootUri(uri); - - let pending = this.pendingContextRequests.get(rootUri.toString()); - if (pending === undefined) { - pending = this.getContextCore(rootUri); - this.pendingContextRequests.set(rootUri.toString(), pending); - } - - try { - return await pending; - } finally { - this.pendingContextRequests.delete(rootUri.toString()); - } - } - - private readonly rootUriToContextMap = new Map(); - - private async getContextCore(rootUri: Uri): Promise { - const key = rootUri.toString(); - let context = this.rootUriToContextMap.get(key); - - // Check if we have a cached a context - if (context?.sha !== undefined) { - return context; - } - - // Check if we have a saved context - context = this.context.get(rootUri); - if (context?.sha !== undefined) { - this.rootUriToContextMap.set(key, context); - - return context; - } - - const { ref } = fromGitHubUri(rootUri); - - // If the requested ref looks like a sha, then use it - if (isSha(ref)) { - context = { requestRef: ref, branch: ref, sha: ref, timestamp: Date.now() }; - } else { - let branch; - if (ref === 'HEAD') { - branch = await this.defaultBranchQuery(rootUri); - if (branch === undefined) { - throw new Error(`Cannot get context for Uri(${rootUri.toString(true)}); unable to get default branch`); - } - } else { - branch = ref; - } - - // Query for the latest sha for the give ref - const sha = await this.latestCommitQuery(rootUri); - context = { requestRef: ref, branch: branch, sha: sha, timestamp: Date.now() }; - } - - this.updateContext(rootUri, context); - - return context; - } - - private updateContext(rootUri: Uri, context: GitHubApiContext) { - this.rootUriToContextMap.set(rootUri.toString(), context); - this.context.set(rootUri, context); - } -} - -interface GitHubSearchTextMatch { - object_url: string; - object_type: string; - property: string; - fragment: string; - matches: { - text: string; - indices: number[]; - }[]; -} - -interface SearchQueryMatch { - path: string; - ranges: Range[]; - preview: string; - matches: Range[]; -} - -interface SearchQueryResults { - matches: SearchQueryMatch[]; - limitHit: boolean; -} diff --git a/extensions/github-browser/src/github/fs.ts b/extensions/github-browser/src/github/fs.ts deleted file mode 100644 index d0af10751c0..00000000000 --- a/extensions/github-browser/src/github/fs.ts +++ /dev/null @@ -1,332 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; -import { - CancellationToken, - Disposable, - Event, - EventEmitter, - FileChangeEvent, - FileSearchOptions, - FileSearchProvider, - FileSearchQuery, - FileStat, - FileSystemError, - FileSystemProvider, - FileType, - Progress, - TextSearchComplete, - TextSearchOptions, - TextSearchProvider, - TextSearchQuery, - TextSearchResult, - Uri, - workspace, -} from 'vscode'; -import * as fuzzySort from 'fuzzysort'; -import fetch from 'node-fetch'; -import { GitHubApi } from './api'; -import { Iterables } from '../iterables'; -import { getRootUri } from '../extension'; - -const emptyDisposable = { dispose: () => { /* noop */ } }; -const replaceBackslashRegex = /(\/|\\)/g; -const textEncoder = new TextEncoder(); - -interface Fuzzysort extends Fuzzysort.Fuzzysort { - prepareSlow(target: string): Fuzzysort.Prepared; - cleanup(): void; -} - -export class GitHubFS implements FileSystemProvider, FileSearchProvider, TextSearchProvider, Disposable { - static scheme = 'github'; - - private _onDidChangeFile = new EventEmitter(); - get onDidChangeFile(): Event { - return this._onDidChangeFile.event; - } - - private readonly disposable: Disposable; - private fsCache = new Map>(); - - constructor(private readonly github: GitHubApi) { - this.disposable = Disposable.from( - workspace.registerFileSystemProvider(GitHubFS.scheme, this, { - isCaseSensitive: true, - isReadonly: true - }), - workspace.registerFileSearchProvider(GitHubFS.scheme, this), - workspace.registerTextSearchProvider(GitHubFS.scheme, this), - github.onDidChangeContext(e => this.fsCache.delete(e.toString())) - ); - } - - dispose() { - this.disposable?.dispose(); - } - - private getCache(uri: Uri) { - const rootUri = getRootUri(uri); - if (rootUri === undefined) { - return undefined; - } - - let cache = this.fsCache.get(rootUri.toString()); - if (cache === undefined) { - cache = new Map(); - this.fsCache.set(rootUri.toString(), cache); - } - return cache; - } - - //#region FileSystemProvider - - watch(): Disposable { - return emptyDisposable; - } - - async stat(uri: Uri): Promise { - if (uri.path === '' || uri.path.lastIndexOf('/') === 0) { - const context = await this.github.getContext(uri); - return { type: FileType.Directory, size: 0, ctime: 0, mtime: context?.timestamp }; - } - - const data = await this.fsQuery<{ - __typename: string; - byteSize: number | undefined; - }>( - uri, - `__typename - ...on Blob { - byteSize - }`, - this.getCache(uri), - ); - - if (data === undefined) { - throw FileSystemError.FileNotFound(); - } - - const context = await this.github.getContext(uri); - - return { - type: typenameToFileType(data.__typename), - size: data.byteSize ?? 0, - ctime: 0, - mtime: context?.timestamp, - }; - } - - async readDirectory(uri: Uri): Promise<[string, FileType][]> { - const data = await this.fsQuery<{ - entries: { name: string; type: string }[]; - }>( - uri, - `... on Tree { - entries { - name - type - } - }`, - this.getCache(uri), - ); - - return (data?.entries ?? []).map<[string, FileType]>(e => [ - e.name, - typenameToFileType(e.type), - ]); - } - - createDirectory(_uri: Uri): void | Thenable { - throw FileSystemError.NoPermissions(); - } - - async readFile(uri: Uri): Promise { - const data = await this.fsQuery<{ - oid: string; - isBinary: boolean; - text: string; - }>( - uri, - `... on Blob { - oid, - isBinary, - text - }`, - ); - - if (data?.isBinary) { - const { owner, repo, path } = fromGitHubUri(uri); - // e.g. https://raw.githubusercontent.com/eamodio/vscode-gitlens/HEAD/images/gitlens-icon.png - const downloadUri = uri.with({ - scheme: 'https', - authority: 'raw.githubusercontent.com', - path: `/${owner}/${repo}/HEAD/${path}`, - }); - - return downloadBinary(downloadUri); - } - - return textEncoder.encode(data?.text ?? ''); - } - - async writeFile(_uri: Uri, _content: Uint8Array, _options: { create: boolean, overwrite: boolean }): Promise { - throw FileSystemError.NoPermissions(); - } - - delete(_uri: Uri, _options: { recursive: boolean }): void | Thenable { - throw FileSystemError.NoPermissions(); - } - - rename(_oldUri: Uri, _newUri: Uri, _options: { overwrite: boolean }): void | Thenable { - throw FileSystemError.NoPermissions(); - } - - copy(_source: Uri, _destination: Uri, _options: { overwrite: boolean }): void | Thenable { - throw FileSystemError.NoPermissions(); - } - - //#endregion - - //#region FileSearchProvider - - private fileSearchCache = new Map(); - - async provideFileSearchResults( - query: FileSearchQuery, - options: FileSearchOptions, - token: CancellationToken, - ): Promise { - let searchable = this.fileSearchCache.get(options.folder.toString(true)); - if (searchable === undefined) { - const matches = await this.github.filesQuery(options.folder); - if (matches === undefined || token.isCancellationRequested) { - return []; - } - - searchable = [...Iterables.map(matches, m => (fuzzySort as Fuzzysort).prepareSlow(m))]; - this.fileSearchCache.set(options.folder.toString(true), searchable); - } - - if (options.maxResults === undefined || options.maxResults === 0 || options.maxResults >= searchable.length) { - const results = searchable.map(m => Uri.joinPath(options.folder, m.target)); - return results; - } - - const results = fuzzySort - .go(query.pattern.replace(replaceBackslashRegex, '/'), searchable, { - allowTypo: true, - limit: options.maxResults, - }) - .map(m => Uri.joinPath(options.folder, m.target)); - - (fuzzySort as Fuzzysort).cleanup(); - - return results; - } - - //#endregion - - //#region TextSearchProvider - - async provideTextSearchResults( - query: TextSearchQuery, - options: TextSearchOptions, - progress: Progress, - _token: CancellationToken, - ): Promise { - const results = await this.github.searchQuery( - query.pattern, - options.folder, - { maxResults: options.maxResults, context: { before: options.beforeContext, after: options.afterContext } }, - ); - if (results === undefined) { return { limitHit: true }; } - - let uri; - for (const m of results.matches) { - uri = Uri.joinPath(options.folder, m.path); - - progress.report({ - uri: uri, - ranges: m.ranges, - preview: { - text: m.preview, - matches: m.matches, - }, - }); - } - - return { limitHit: false }; - } - - //#endregion - - private async fsQuery(uri: Uri, query: string, cache?: Map): Promise { - const key = `${uri.toString()}:${getHashCode(query)}`; - - let data = cache?.get(key); - if (data !== undefined) { - return data as T; - } - - data = await this.github.fsQuery(uri, query); - cache?.set(key, data); - return data; - } -} - -async function downloadBinary(uri: Uri) { - const resp = await fetch(uri.toString()); - const array = new Uint8Array(await resp.arrayBuffer()); - return array; -} - -function typenameToFileType(typename: string | undefined | null) { - if (typename) { - typename = typename.toLocaleLowerCase(); - } - - switch (typename) { - case 'blob': - return FileType.File; - case 'tree': - return FileType.Directory; - default: - return FileType.Unknown; - } -} - -type RepoInfo = { owner: string; repo: string; path: string | undefined; ref: string }; -export function fromGitHubUri(uri: Uri): RepoInfo { - const [, owner, repo, ...rest] = uri.path.split('/'); - - let ref; - if (uri.authority) { - ref = uri.authority; - // The casing of HEAD is important for the GitHub api to work - if (/HEAD/i.test(ref)) { - ref = 'HEAD'; - } - } - return { owner: owner, repo: repo, path: rest.join('/'), ref: ref ?? 'HEAD' }; -} - -function getHashCode(s: string): number { - let hash = 0; - - if (s.length === 0) { - return hash; - } - - let char; - const len = s.length; - for (let i = 0; i < len; i++) { - char = s.charCodeAt(i); - hash = ((hash << 5) - hash) + char; - hash |= 0; // Convert to 32bit integer - } - return hash; -} diff --git a/extensions/github-browser/src/iterables.ts b/extensions/github-browser/src/iterables.ts deleted file mode 100644 index 5679e2c81ec..00000000000 --- a/extensions/github-browser/src/iterables.ts +++ /dev/null @@ -1,29 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -export namespace Iterables { - export function* filterMap( - source: Iterable | IterableIterator, - predicateMapper: (item: T) => TMapped | undefined | null, - ): Iterable { - for (const item of source) { - const mapped = predicateMapper(item); - if (mapped !== undefined && mapped !== null) { - yield mapped; - } - } - } - - export function* map( - source: Iterable | IterableIterator, - mapper: (item: T) => TMapped, - ): Iterable { - for (const item of source) { - yield mapper(item); - } - } -} diff --git a/extensions/github-browser/src/scm.ts b/extensions/github-browser/src/scm.ts deleted file mode 100644 index 7a8e4292f22..00000000000 --- a/extensions/github-browser/src/scm.ts +++ /dev/null @@ -1,177 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; -import { CancellationToken, commands, Disposable, scm, SourceControl, SourceControlResourceGroup, SourceControlResourceState, Uri, window, workspace } from 'vscode'; -import * as nls from 'vscode-nls'; -import { IChangeStore } from './changeStore'; -import { GitHubApi, CommitOperation } from './github/api'; -import { getRelativePath } from './extension'; - -const localize = nls.loadMessageBundle(); - -interface ScmProvider { - sourceControl: SourceControl, - groups: SourceControlResourceGroup[] -} - -export class VirtualSCM implements Disposable { - private readonly providers: ScmProvider[] = []; - - private disposable: Disposable; - - constructor( - private readonly originalScheme: string, - private readonly github: GitHubApi, - private readonly changeStore: IChangeStore, - ) { - this.registerCommands(); - - // TODO@eamodio listen for workspace folder changes - for (const folder of workspace.workspaceFolders ?? []) { - this.createScmProvider(folder.uri, folder.name); - - for (const operation of changeStore.getChanges(folder.uri)) { - this.update(folder.uri, operation.uri); - } - } - - this.disposable = Disposable.from( - changeStore.onDidChange(e => this.update(e.rootUri, e.uri)), - ); - } - - dispose() { - this.disposable.dispose(); - } - - private registerCommands() { - commands.registerCommand('githubBrowser.commit', (sourceControl: SourceControl | undefined) => { - // TODO@eamodio remove this hack once I figure out why the args are missing - if (sourceControl === undefined && this.providers.length === 1) { - sourceControl = this.providers[0].sourceControl; - } - - if (sourceControl === undefined) { - return; - } - - this.commitChanges(sourceControl); - }); - - commands.registerCommand('githubBrowser.discardChanges', (resourceState: SourceControlResourceState) => - this.discardChanges(resourceState.resourceUri) - ); - - commands.registerCommand('githubBrowser.openChanges', (resourceState: SourceControlResourceState) => - this.openChanges(resourceState.resourceUri) - ); - - commands.registerCommand('githubBrowser.openFile', (resourceState: SourceControlResourceState) => - this.openFile(resourceState.resourceUri) - ); - } - - async commitChanges(sourceControl: SourceControl): Promise { - const operations = this.changeStore - .getChanges(sourceControl.rootUri!) - .map(operation => { - const path = getRelativePath(sourceControl.rootUri!, operation.uri); - switch (operation.type) { - case 'created': - return { type: operation.type, path: path, content: this.changeStore.getContent(operation.uri)! }; - case 'changed': - return { type: operation.type, path: path, content: this.changeStore.getContent(operation.uri)! }; - case 'deleted': - return { type: operation.type, path: path }; - } - }); - if (!operations.length) { - window.showInformationMessage(localize('no changes', "There are no changes to commit.")); - - return; - } - - const message = sourceControl.inputBox.value; - if (message) { - const sha = await this.github.commit(this.getOriginalResource(sourceControl.rootUri!), message, operations); - if (sha !== undefined) { - this.changeStore.acceptAll(sourceControl.rootUri!); - sourceControl.inputBox.value = ''; - } - } - } - - discardChanges(uri: Uri): Promise { - return this.changeStore.discard(uri); - } - - openChanges(uri: Uri) { - return this.changeStore.openChanges(uri, this.getOriginalResource(uri)); - } - - openFile(uri: Uri) { - return this.changeStore.openFile(uri); - } - - private update(rootUri: Uri, uri: Uri) { - const folder = workspace.getWorkspaceFolder(uri); - if (folder === undefined) { - return; - } - - const provider = this.createScmProvider(rootUri, folder.name); - const group = this.createChangesGroup(provider); - group.resourceStates = this.changeStore.getChanges(rootUri).map(op => { - const rs: SourceControlResourceState = { - decorations: { - strikeThrough: op.type === 'deleted' - }, - resourceUri: op.uri, - command: { - command: 'githubBrowser.openChanges', - title: 'Open Changes', - } - }; - rs.command!.arguments = [rs]; - return rs; - }); - } - - private createScmProvider(rootUri: Uri, name: string) { - let provider = this.providers.find(sc => sc.sourceControl.rootUri?.toString() === rootUri.toString()); - if (provider === undefined) { - const sourceControl = scm.createSourceControl('github', name, rootUri); - sourceControl.quickDiffProvider = { provideOriginalResource: uri => this.getOriginalResource(uri) }; - sourceControl.acceptInputCommand = { - command: 'githubBrowser.commit', - title: 'Commit', - arguments: [sourceControl] - }; - sourceControl.inputBox.placeholder = `Message (Ctrl+Enter to commit '${name}')`; - // sourceControl.inputBox.validateInput = value => value ? undefined : 'Invalid commit message'; - - provider = { sourceControl: sourceControl, groups: [] }; - this.createChangesGroup(provider); - this.providers.push(provider); - } - - return provider; - } - - private createChangesGroup(provider: ScmProvider) { - let group = provider.groups.find(g => g.id === 'github.changes'); - if (group === undefined) { - group = provider.sourceControl.createResourceGroup('github.changes', 'Changes'); - provider.groups.push(group); - } - - return group; - } - - private getOriginalResource(uri: Uri, _token?: CancellationToken): Uri { - return uri.with({ scheme: this.originalScheme }); - } -} diff --git a/extensions/github-browser/src/sha1.ts b/extensions/github-browser/src/sha1.ts deleted file mode 100644 index 0469b3c98a1..00000000000 --- a/extensions/github-browser/src/sha1.ts +++ /dev/null @@ -1,29 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -const textDecoder = new TextDecoder(); -const textEncoder = new TextEncoder(); - -declare let WEBWORKER: boolean; - -export async function sha1(s: string | Uint8Array): Promise { - while (true) { - try { - if (WEBWORKER) { - const hash = await globalThis.crypto.subtle.digest({ name: 'sha-1' }, typeof s === 'string' ? textEncoder.encode(s) : s); - // Use encodeURIComponent to avoid issues with btoa and Latin-1 characters - return globalThis.btoa(encodeURIComponent(textDecoder.decode(hash))); - } else { - return (await import('crypto')).createHash('sha1').update(s).digest('base64'); - } - } catch (ex) { - if (ex instanceof ReferenceError) { - (global as any).WEBWORKER = false; - } - } - } -} diff --git a/extensions/github-browser/src/statusbar.ts b/extensions/github-browser/src/statusbar.ts deleted file mode 100644 index e5a049a8631..00000000000 --- a/extensions/github-browser/src/statusbar.ts +++ /dev/null @@ -1,99 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; -import { Disposable, StatusBarAlignment, StatusBarItem, Uri, window, workspace } from 'vscode'; -import { ChangeStoreEvent, IChangeStore } from './changeStore'; -import { GitHubApiContext } from './github/api'; -import { isSha } from './extension'; -import { ContextStore, WorkspaceFolderContext } from './contextStore'; - -export class StatusBar implements Disposable { - private readonly disposable: Disposable; - - private readonly items = new Map(); - - constructor( - private readonly contextStore: ContextStore, - private readonly changeStore: IChangeStore - ) { - this.disposable = Disposable.from( - contextStore.onDidChange(this.onContextsChanged, this), - changeStore.onDidChange(this.onChanged, this) - ); - - for (const context of this.contextStore.getForWorkspace()) { - this.createOrUpdateStatusBarItem(context); - } - } - - dispose() { - this.disposable?.dispose(); - this.items.forEach(i => i.dispose()); - } - - private createOrUpdateStatusBarItem(wc: WorkspaceFolderContext) { - let item = this.items.get(wc.folderUri.toString()); - if (item === undefined) { - item = window.createStatusBarItem({ - id: `githubBrowser.branch:${wc.folderUri.toString()}`, - name: `GitHub Browser: ${wc.name}`, - alignment: StatusBarAlignment.Left, - priority: 1000 - }); - } - - if (isSha(wc.context.branch)) { - item.text = `$(git-commit) ${wc.context.branch.substr(0, 8)}`; - item.tooltip = `${wc.name} \u2022 ${wc.context.branch.substr(0, 8)}`; - } else { - item.text = `$(git-branch) ${wc.context.branch}`; - item.tooltip = `${wc.name} \u2022 ${wc.context.branch}${wc.context.sha ? ` @ ${wc.context.sha?.substr(0, 8)}` : ''}`; - } - - const hasChanges = this.changeStore.hasChanges(wc.folderUri); - if (hasChanges) { - item.text += '*'; - } - - item.show(); - - this.items.set(wc.folderUri.toString(), item); - } - - private onContextsChanged(uri: Uri) { - const folder = workspace.getWorkspaceFolder(this.contextStore.getWorkspaceResource(uri)); - if (folder === undefined) { - return; - } - - const context = this.contextStore.get(uri); - if (context === undefined) { - return; - } - - this.createOrUpdateStatusBarItem({ - context: context, - name: folder.name, - folderUri: folder.uri, - }); - } - - private onChanged(e: ChangeStoreEvent) { - const item = this.items.get(e.rootUri.toString()); - if (item !== undefined) { - const hasChanges = this.changeStore.hasChanges(e.rootUri); - if (hasChanges) { - if (!item.text.endsWith('*')) { - item.text += '*'; - } - } else { - if (item.text.endsWith('*')) { - item.text = item.text.substr(0, item.text.length - 1); - } - } - } - } -} diff --git a/extensions/github-browser/yarn.lock b/extensions/github-browser/yarn.lock deleted file mode 100644 index 2c8f1ad9658..00000000000 --- a/extensions/github-browser/yarn.lock +++ /dev/null @@ -1,332 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@octokit/auth-token@^2.4.0": - version "2.4.2" - resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.2.tgz#10d0ae979b100fa6b72fa0e8e63e27e6d0dbff8a" - integrity sha512-jE/lE/IKIz2v1+/P0u4fJqv0kYwXOTujKemJMFr6FeopsxlIK3+wKDCJGnysg81XID5TgZQbIfuJ5J0lnTiuyQ== - dependencies: - "@octokit/types" "^5.0.0" - -"@octokit/core@^3.0.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.1.0.tgz#9c3c9b23f7504668cfa057f143ccbf0c645a0ac9" - integrity sha512-yPyQSmxIXLieEIRikk2w8AEtWkFdfG/LXcw1KvEtK3iP0ENZLW/WYQmdzOKqfSaLhooz4CJ9D+WY79C8ZliACw== - dependencies: - "@octokit/auth-token" "^2.4.0" - "@octokit/graphql" "^4.3.1" - "@octokit/request" "^5.4.0" - "@octokit/types" "^5.0.0" - before-after-hook "^2.1.0" - universal-user-agent "^5.0.0" - -"@octokit/endpoint@^6.0.1": - version "6.0.3" - resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.3.tgz#dd09b599662d7e1b66374a177ab620d8cdf73487" - integrity sha512-Y900+r0gIz+cWp6ytnkibbD95ucEzDSKzlEnaWS52hbCDNcCJYO5mRmWW7HRAnDc7am+N/5Lnd8MppSaTYx1Yg== - dependencies: - "@octokit/types" "^5.0.0" - is-plain-object "^3.0.0" - universal-user-agent "^5.0.0" - -"@octokit/graphql@4.5.1", "@octokit/graphql@^4.3.1": - version "4.5.1" - resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.5.1.tgz#162aed1490320b88ce34775b3f6b8de945529fa9" - integrity sha512-qgMsROG9K2KxDs12CO3bySJaYoUu2aic90qpFrv7A8sEBzZ7UFGvdgPKiLw5gOPYEYbS0Xf8Tvf84tJutHPulQ== - dependencies: - "@octokit/request" "^5.3.0" - "@octokit/types" "^5.0.0" - universal-user-agent "^5.0.0" - -"@octokit/plugin-paginate-rest@^2.2.0": - version "2.2.3" - resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.2.3.tgz#a6ad4377e7e7832fb4bdd9d421e600cb7640ac27" - integrity sha512-eKTs91wXnJH8Yicwa30jz6DF50kAh7vkcqCQ9D7/tvBAP5KKkg6I2nNof8Mp/65G0Arjsb4QcOJcIEQY+rK1Rg== - dependencies: - "@octokit/types" "^5.0.0" - -"@octokit/plugin-request-log@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz#eef87a431300f6148c39a7f75f8cfeb218b2547e" - integrity sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw== - -"@octokit/plugin-rest-endpoint-methods@4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.0.0.tgz#b02a2006dda8e908c3f8ab381dd5475ef5a810a8" - integrity sha512-emS6gysz4E9BNi9IrCl7Pm4kR+Az3MmVB0/DoDCmF4U48NbYG3weKyDlgkrz6Jbl4Mu4nDx8YWZwC4HjoTdcCA== - dependencies: - "@octokit/types" "^5.0.0" - deprecation "^2.3.1" - -"@octokit/request-error@^2.0.0": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.0.2.tgz#0e76b83f5d8fdda1db99027ea5f617c2e6ba9ed0" - integrity sha512-2BrmnvVSV1MXQvEkrb9zwzP0wXFNbPJij922kYBTLIlIafukrGOb+ABBT2+c6wZiuyWDH1K1zmjGQ0toN/wMWw== - dependencies: - "@octokit/types" "^5.0.1" - deprecation "^2.0.0" - once "^1.4.0" - -"@octokit/request@^5.3.0", "@octokit/request@^5.4.0": - version "5.4.5" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.5.tgz#8df65bd812047521f7e9db6ff118c06ba84ac10b" - integrity sha512-atAs5GAGbZedvJXXdjtKljin+e2SltEs48B3naJjqWupYl2IUBbB/CJisyjbNHcKpHzb3E+OYEZ46G8eakXgQg== - dependencies: - "@octokit/endpoint" "^6.0.1" - "@octokit/request-error" "^2.0.0" - "@octokit/types" "^5.0.0" - deprecation "^2.0.0" - is-plain-object "^3.0.0" - node-fetch "^2.3.0" - once "^1.4.0" - universal-user-agent "^5.0.0" - -"@octokit/rest@18.0.0": - version "18.0.0" - resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.0.0.tgz#7f401d9ce13530ad743dfd519ae62ce49bcc0358" - integrity sha512-4G/a42lry9NFGuuECnua1R1eoKkdBYJap97jYbWDNYBOUboWcM75GJ1VIcfvwDV/pW0lMPs7CEmhHoVrSV5shg== - dependencies: - "@octokit/core" "^3.0.0" - "@octokit/plugin-paginate-rest" "^2.2.0" - "@octokit/plugin-request-log" "^1.0.0" - "@octokit/plugin-rest-endpoint-methods" "4.0.0" - -"@octokit/types@^5.0.0", "@octokit/types@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-5.0.1.tgz#5459e9a5e9df8565dcc62c17a34491904d71971e" - integrity sha512-GorvORVwp244fGKEt3cgt/P+M0MGy4xEDbckw+K5ojEezxyMDgCaYPKVct+/eWQfZXOT7uq0xRpmrl/+hliabA== - dependencies: - "@types/node" ">= 8" - -"@types/node-fetch@2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c" - integrity sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw== - dependencies: - "@types/node" "*" - form-data "^3.0.0" - -"@types/node@*", "@types/node@>= 8": - version "14.0.14" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.14.tgz#24a0b5959f16ac141aeb0c5b3cd7a15b7c64cbce" - integrity sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ== - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= - -before-after-hook@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" - integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== - -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -cross-spawn@^6.0.0: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - -deprecation@^2.0.0, deprecation@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" - integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== - -end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -form-data@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" - integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - -fuzzysort@1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/fuzzysort/-/fuzzysort-1.1.4.tgz#a0510206ed44532cbb52cf797bf5a3cb12acd4ba" - integrity sha512-JzK/lHjVZ6joAg3OnCjylwYXYVjRiwTY6Yb25LvfpJHK8bjisfnZJ5bY8aVWwTwCXgxPNgLAtmHL+Hs5q1ddLQ== - -get-stream@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - -is-plain-object@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.1.tgz#662d92d24c0aa4302407b0d45d21f2251c85f85b" - integrity sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g== - -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -macos-release@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f" - integrity sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA== - -mime-db@1.44.0: - version "1.44.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== - -mime-types@^2.1.12: - version "2.1.27" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== - dependencies: - mime-db "1.44.0" - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - -node-fetch@2.6.0, node-fetch@^2.3.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" - integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== - -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= - dependencies: - path-key "^2.0.0" - -once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -os-name@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801" - integrity sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg== - dependencies: - macos-release "^2.2.0" - windows-release "^3.1.0" - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= - -path-key@^2.0.0, path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -semver@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -signal-exit@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= - -universal-user-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-5.0.0.tgz#a3182aa758069bf0e79952570ca757de3579c1d9" - integrity sha512-B5TPtzZleXyPrUMKCpEHFmVhMN6EhmJYjG5PQna9s7mXeSqGTLap4OpqLl5FCEFUI3UBmllkETwKf/db66Y54Q== - dependencies: - os-name "^3.1.0" - -vscode-nls@4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.2.tgz#ca8bf8bb82a0987b32801f9fddfdd2fb9fd3c167" - integrity sha512-7bOHxPsfyuCqmP+hZXscLhiHwe7CSuFE4hyhbs22xPIhQ4jv99FcR4eBzfYYVLP356HNFpdvz63FFb/xw6T4Iw== - -which@^1.2.9: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -windows-release@^3.1.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.3.1.tgz#cb4e80385f8550f709727287bf71035e209c4ace" - integrity sha512-Pngk/RDCaI/DkuHPlGTdIkDiTAnAkyMjoQMZqRsxydNl1qGXNIoZrB7RK8g53F2tEgQBMqQJHQdYZuQEEAu54A== - dependencies: - execa "^1.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= diff --git a/extensions/github/src/publish.ts b/extensions/github/src/publish.ts index 0e8dc89b3ad..9cd79a749b7 100644 --- a/extensions/github/src/publish.ts +++ b/extensions/github/src/publish.ts @@ -140,9 +140,11 @@ export async function publishRepository(gitAPI: GitAPI, repository?: Repository) const ignored = new Set(children); result.forEach(c => ignored.delete(c.label)); - const raw = [...ignored].map(i => `/${i}`).join('\n'); - const encoder = new TextEncoder(); - await vscode.workspace.fs.writeFile(gitignore, encoder.encode(raw)); + if (ignored.size > 0) { + const raw = [...ignored].map(i => `/${i}`).join('\n'); + const encoder = new TextEncoder(); + await vscode.workspace.fs.writeFile(gitignore, encoder.encode(raw)); + } } finally { quickpick.dispose(); } diff --git a/extensions/gulp/src/main.ts b/extensions/gulp/src/main.ts index 534756ac81d..0d70cdd4828 100644 --- a/extensions/gulp/src/main.ts +++ b/extensions/gulp/src/main.ts @@ -148,9 +148,12 @@ class FolderDetector { } let gulpfile = path.join(rootPath, 'gulpfile.js'); if (!await exists(gulpfile)) { - gulpfile = path.join(rootPath, 'gulpfile.babel.js'); - if (! await exists(gulpfile)) { - return emptyTasks; + gulpfile = path.join(rootPath, 'Gulpfile.js'); + if (!await exists(gulpfile)) { + gulpfile = path.join(rootPath, 'gulpfile.babel.js'); + if (!await exists(gulpfile)) { + return emptyTasks; + } } } diff --git a/extensions/html-language-features/.vscodeignore b/extensions/html-language-features/.vscodeignore index 4215adf6eca..a4a4702c351 100644 --- a/extensions/html-language-features/.vscodeignore +++ b/extensions/html-language-features/.vscodeignore @@ -16,5 +16,7 @@ server/.npmignore yarn.lock server/extension.webpack.config.js extension.webpack.config.js +server/extension-browser.webpack.config.js +extension-browser.webpack.config.js CONTRIBUTING.md cgmanifest.json diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index d7e2a8b28fb..eadd61b33d0 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -9,7 +9,7 @@ }, "main": "./out/node/htmlServerMain", "dependencies": { - "vscode-css-languageservice": "^4.3.0", + "vscode-css-languageservice": "^4.3.1", "vscode-html-languageservice": "^3.1.0", "vscode-languageserver": "7.0.0-next.3", "vscode-nls": "^4.1.2", diff --git a/extensions/html-language-features/server/src/test/completions.test.ts b/extensions/html-language-features/server/src/test/completions.test.ts index b491ad28836..0371a2f74cd 100644 --- a/extensions/html-language-features/server/src/test/completions.test.ts +++ b/extensions/html-language-features/server/src/test/completions.test.ts @@ -57,7 +57,7 @@ export async function testCompletionFor(value: string, expected: { count?: numbe let document = TextDocument.create(uri, 'html', 0, value); let position = document.positionAt(offset); - const context = getDocumentContext(uri, workspace.folders) + const context = getDocumentContext(uri, workspace.folders); const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService()); const mode = languageModes.getModeAtPosition(document, position)!; diff --git a/extensions/html-language-features/server/yarn.lock b/extensions/html-language-features/server/yarn.lock index 0778b5468ac..dc001b1ada8 100644 --- a/extensions/html-language-features/server/yarn.lock +++ b/extensions/html-language-features/server/yarn.lock @@ -418,15 +418,10 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" -lodash@^4.16.4: - version "4.17.10" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" - integrity sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg== - -lodash@^4.17.15: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== +lodash@^4.16.4, lodash@^4.17.15: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== log-symbols@3.0.0: version "3.0.0" @@ -726,10 +721,10 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -vscode-css-languageservice@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.3.0.tgz#40c797d664ab6188cace33cfbb19b037580a9318" - integrity sha512-BkQAMz4oVHjr0oOAz5PdeE72txlLQK7NIwzmclfr+b6fj6I8POwB+VoXvrZLTbWt9hWRgfvgiQRkh5JwrjPJ5A== +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== dependencies: vscode-languageserver-textdocument "^1.0.1" vscode-languageserver-types "3.16.0-next.2" diff --git a/extensions/image-preview/.vscodeignore b/extensions/image-preview/.vscodeignore index 30d948fbc66..bcb886a094d 100644 --- a/extensions/image-preview/.vscodeignore +++ b/extensions/image-preview/.vscodeignore @@ -4,6 +4,7 @@ tsconfig.json out/test/** out/** extension.webpack.config.js +extension-browser.webpack.config.js cgmanifest.json yarn.lock preview-src/** diff --git a/extensions/image-preview/package.json b/extensions/image-preview/package.json index dd04cc77771..064ada94858 100644 --- a/extensions/image-preview/package.json +++ b/extensions/image-preview/package.json @@ -4,7 +4,8 @@ "description": "%description%", "extensionKind": [ "ui", - "workspace" + "workspace", + "web" ], "version": "1.0.0", "publisher": "vscode", diff --git a/extensions/ini/package.json b/extensions/ini/package.json index cc5ca5da606..24d86072749 100644 --- a/extensions/ini/package.json +++ b/extensions/ini/package.json @@ -18,8 +18,8 @@ }, { "id": "properties", - "extensions": [ ".properties", ".cfg", ".conf", ".directory" ], - "filenames": [ ".gitattributes", ".gitconfig", "gitconfig", ".gitmodules", ".editorconfig" ], + "extensions": [ ".properties", ".cfg", ".conf", ".directory", ".gitattributes", ".gitconfig", ".gitmodules", ".editorconfig" ], + "filenames": [ "gitconfig" ], "filenamePatterns": [ "**/.config/git/config", "**/.git/config" ], "aliases": [ "Properties", "properties" ], "configuration": "./properties.language-configuration.json" diff --git a/extensions/javascript/snippets/javascript.code-snippets b/extensions/javascript/snippets/javascript.code-snippets index fc892b57e92..b005c80c844 100644 --- a/extensions/javascript/snippets/javascript.code-snippets +++ b/extensions/javascript/snippets/javascript.code-snippets @@ -180,14 +180,14 @@ "Log warning to console": { "prefix": "warn", "body": [ - "console.warn($1);", + "console.warn($1);" ], "description": "Log warning to the console" }, "Log error to console": { "prefix": "error", "body": [ - "console.error($1);", + "console.error($1);" ], "description": "Log error to the console" } diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index eb2b984af06..e3e507c7b6c 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.7.0", + "vscode-json-languageservice": "^3.8.0", "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 e7dffe1d702..281f8276eb3 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.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-3.7.0.tgz#0174417f139cf41dd60c84538fd052385bfb46f6" - integrity sha512-nGLqcBhTjdfkl8Dz9sYGK/ZCTjscYFoIjYw+qqkWB+vyNfM0k/AyIoT73DQvB/PArteCKjEVfQUF72GRZEDSbQ== +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== dependencies: jsonc-parser "^2.2.1" vscode-languageserver-textdocument "^1.0.1" diff --git a/extensions/markdown-language-features/.vscodeignore b/extensions/markdown-language-features/.vscodeignore index bcb886a094d..9f1e0620775 100644 --- a/extensions/markdown-language-features/.vscodeignore +++ b/extensions/markdown-language-features/.vscodeignore @@ -1,4 +1,5 @@ test/** +test-workspace/** src/** tsconfig.json out/test/** diff --git a/extensions/markdown-language-features/media/index.js b/extensions/markdown-language-features/media/index.js index 4075748e35b..0433f33c89a 100644 --- a/extensions/markdown-language-features/media/index.js +++ b/extensions/markdown-language-features/media/index.js @@ -1 +1 @@ -!function(e){var t={};function n(o){if(t[o])return t[o].exports;var i=t[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(o,i,function(t){return e[t]}.bind(null,i));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=3)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});let o=void 0;function i(e){const t=document.getElementById("vscode-markdown-preview-data");if(t){const n=t.getAttribute(e);if(n)return JSON.parse(n)}throw new Error(`Could not load data for ${e}`)}t.getData=i,t.getSettings=function(){if(o)return o;if(o=i("data-settings"))return o;throw new Error("Could not load settings")}},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const o=n(0),i="code-line";function r(e){return t=0,n=o.getSettings().lineCount-1,i=e,Math.min(n,Math.max(t,i));var t,n,i}const c=(()=>{let e;return()=>{if(!e){e=[{element:document.body,line:0}];for(const t of document.getElementsByClassName(i)){const n=+t.getAttribute("data-line");isNaN(n)||("CODE"===t.tagName&&t.parentElement&&"PRE"===t.parentElement.tagName?e.push({element:t.parentElement,line:n}):e.push({element:t,line:n}))}}return e}})();function s(e){const t=Math.floor(e),n=c();let o=n[0]||null;for(const e of n){if(e.line===t)return{previous:e,next:void 0};if(e.line>t)return{previous:o,next:e};o=e}return{previous:o}}function a(e){const t=c(),n=e-window.scrollY;let o=-1,i=t.length-1;for(;o+1=n?i=e:o=e}const r=t[i],s=u(r);if(i>=1&&s.top>n){return{previous:t[o],next:r}}return i>1&&in?{previous:r,next:t[i+1]}:{previous:r}}function u({element:e}){const t=e.getBoundingClientRect(),n=e.querySelector(`.${i}`);if(n){const e=n.getBoundingClientRect(),o=Math.max(1,e.top-t.top);return{top:t.top,height:o}}return t}t.getElementsForSourceLine=s,t.getLineElementsAtPageOffset=a,t.scrollToRevealSourceLine=function(e){if(!o.getSettings().scrollPreviewWithEditor)return;if(e<=0)return void window.scroll(window.scrollX,0);const{previous:t,next:n}=s(e);if(!t)return;let i=0;const r=u(t),c=r.top;if(n&&n.line!==t.line){i=c+(e-t.line)/(n.line-t.line)*(n.element.getBoundingClientRect().top-c)}else{const t=e-Math.floor(e);i=c+r.height*t}window.scroll(window.scrollX,Math.max(1,window.scrollY+i))},t.getEditorLineNumberForPageOffset=function(e){const{previous:t,next:n}=a(e);if(t){const o=u(t),i=e-window.scrollY-o.top;if(n){const e=i/(u(n).top-o.top);return r(t.line+e*(n.line-t.line))}{const e=i/o.height;return r(t.line+e)}}return null},t.getLineElementForFragment=function(e){return c().find(t=>t.element.id===e)}},function(e,t,n){"use strict";(function(e){Object.defineProperty(t,"__esModule",{value:!0});const o=n(7),i=n(8),r=n(9),c=n(2),s=n(0),a=n(10);let u=!0;const l=new o.ActiveLineMarker,f=s.getSettings(),d=acquireVsCodeApi(),m=d.getState(),p={..."object"==typeof m?m:{},...s.getData("data-state")};d.setState(p);const g=r.createPosterForVsCode(d);window.cspAlerter.setPoster(g),window.styleLoadingMonitor.setPoster(g),window.onload=()=>{v()},i.onceDocumentLoaded(()=>{const t=p.scrollProgress;"number"!=typeof t||f.fragment?f.scrollPreviewWithEditor&&e(()=>{if(f.fragment){p.fragment=void 0,d.setState(p);const e=c.getLineElementForFragment(f.fragment);e&&(u=!0,c.scrollToRevealSourceLine(e.line))}else isNaN(f.line)||(u=!0,c.scrollToRevealSourceLine(f.line))}):e(()=>{u=!0,window.scrollTo(0,t*document.body.clientHeight)})});const h=(()=>{const e=a(e=>{u=!0,c.scrollToRevealSourceLine(e)},50);return t=>{isNaN(t)||(p.line=t,e(t))}})();let v=a(()=>{const e=[];let t=document.getElementsByTagName("img");if(t){let n;for(n=0;n{u=!0,w(),v()},!0),window.addEventListener("message",e=>{if(e.data.source===f.source)switch(e.data.type){case"onDidChangeTextEditorSelection":l.onDidChangeTextEditorSelection(e.data.line);break;case"updateView":h(e.data.line)}},!1),document.addEventListener("dblclick",e=>{if(!f.doubleClickToSwitchToEditor)return;for(let t=e.target;t;t=t.parentNode)if("A"===t.tagName)return;const t=e.pageY,n=c.getEditorLineNumberForPageOffset(t);"number"!=typeof n||isNaN(n)||g.postMessage("didClick",{line:Math.floor(n)})});const y=["http:","https:","mailto:","vscode:","vscode-insiders:"];function w(){p.scrollProgress=window.scrollY/document.body.clientHeight,d.setState(p)}document.addEventListener("click",e=>{if(!e)return;let t=e.target;for(;t;){if(t.tagName&&"A"===t.tagName&&t.href){if(t.getAttribute("href").startsWith("#"))return;if(y.some(e=>t.href.startsWith(e)))return;const n=t.getAttribute("data-href")||t.getAttribute("href");return/^[a-z\-]+:/i.test(n)?void 0:(g.postMessage("openLink",{href:n}),e.preventDefault(),void e.stopPropagation())}t=t.parentNode}},!0),window.addEventListener("scroll",a(()=>{if(w(),u)u=!1;else{const e=c.getEditorLineNumberForPageOffset(window.scrollY);"number"!=typeof e||isNaN(e)||g.postMessage("revealLine",{line:e})}},50))}).call(this,n(4).setImmediate)},function(e,t,n){(function(e){var o=Function.prototype.apply;function i(e,t){this._id=e,this._clearFn=t}t.setTimeout=function(){return new i(o.call(setTimeout,window,arguments),clearTimeout)},t.setInterval=function(){return new i(o.call(setInterval,window,arguments),clearInterval)},t.clearTimeout=t.clearInterval=function(e){e&&e.close()},i.prototype.unref=i.prototype.ref=function(){},i.prototype.close=function(){this._clearFn.call(window,this._id)},t.enroll=function(e,t){clearTimeout(e._idleTimeoutId),e._idleTimeout=t},t.unenroll=function(e){clearTimeout(e._idleTimeoutId),e._idleTimeout=-1},t._unrefActive=t.active=function(e){clearTimeout(e._idleTimeoutId);var t=e._idleTimeout;t>=0&&(e._idleTimeoutId=setTimeout((function(){e._onTimeout&&e._onTimeout()}),t))},n(5),t.setImmediate="undefined"!=typeof self&&self.setImmediate||void 0!==e&&e.setImmediate||this&&this.setImmediate,t.clearImmediate="undefined"!=typeof self&&self.clearImmediate||void 0!==e&&e.clearImmediate||this&&this.clearImmediate}).call(this,n(1))},function(e,t,n){(function(e,t){!function(e,n){"use strict";if(!e.setImmediate){var o,i,r,c,s,a=1,u={},l=!1,f=e.document,d=Object.getPrototypeOf&&Object.getPrototypeOf(e);d=d&&d.setTimeout?d:e,"[object process]"==={}.toString.call(e.process)?o=function(e){t.nextTick((function(){p(e)}))}:!function(){if(e.postMessage&&!e.importScripts){var t=!0,n=e.onmessage;return e.onmessage=function(){t=!1},e.postMessage("","*"),e.onmessage=n,t}}()?e.MessageChannel?((r=new MessageChannel).port1.onmessage=function(e){p(e.data)},o=function(e){r.port2.postMessage(e)}):f&&"onreadystatechange"in f.createElement("script")?(i=f.documentElement,o=function(e){var t=f.createElement("script");t.onreadystatechange=function(){p(e),t.onreadystatechange=null,i.removeChild(t),t=null},i.appendChild(t)}):o=function(e){setTimeout(p,0,e)}:(c="setImmediate$"+Math.random()+"$",s=function(t){t.source===e&&"string"==typeof t.data&&0===t.data.indexOf(c)&&p(+t.data.slice(c.length))},e.addEventListener?e.addEventListener("message",s,!1):e.attachEvent("onmessage",s),o=function(t){e.postMessage(c+t,"*")}),d.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),n=0;n1)for(var n=1;nnew class{postMessage(t,n){e.postMessage({type:t,source:o.getSettings().source,body:n})}}},function(e,t,n){(function(t){var n="Expected a function",o=NaN,i="[object Symbol]",r=/^\s+|\s+$/g,c=/^[-+]0x[0-9a-f]+$/i,s=/^0b[01]+$/i,a=/^0o[0-7]+$/i,u=parseInt,l="object"==typeof t&&t&&t.Object===Object&&t,f="object"==typeof self&&self&&self.Object===Object&&self,d=l||f||Function("return this")(),m=Object.prototype.toString,p=Math.max,g=Math.min,h=function(){return d.Date.now()};function v(e,t,o){var i,r,c,s,a,u,l=0,f=!1,d=!1,m=!0;if("function"!=typeof e)throw new TypeError(n);function v(t){var n=i,o=r;return i=r=void 0,l=t,s=e.apply(o,n)}function b(e){var n=e-u;return void 0===u||n>=t||n<0||d&&e-l>=c}function T(){var e=h();if(b(e))return E(e);a=setTimeout(T,function(e){var n=t-(e-u);return d?g(n,c-(e-l)):n}(e))}function E(e){return a=void 0,m&&i?v(e):(i=r=void 0,s)}function _(){var e=h(),n=b(e);if(i=arguments,r=this,u=e,n){if(void 0===a)return function(e){return l=e,a=setTimeout(T,t),f?v(e):s}(u);if(d)return a=setTimeout(T,t),v(u)}return void 0===a&&(a=setTimeout(T,t)),s}return t=w(t)||0,y(o)&&(f=!!o.leading,c=(d="maxWait"in o)?p(w(o.maxWait)||0,t):c,m="trailing"in o?!!o.trailing:m),_.cancel=function(){void 0!==a&&clearTimeout(a),l=0,i=u=r=a=void 0},_.flush=function(){return void 0===a?s:E(h())},_}function y(e){var t=typeof e;return!!e&&("object"==t||"function"==t)}function w(e){if("number"==typeof e)return e;if(function(e){return"symbol"==typeof e||function(e){return!!e&&"object"==typeof e}(e)&&m.call(e)==i}(e))return o;if(y(e)){var t="function"==typeof e.valueOf?e.valueOf():e;e=y(t)?t+"":t}if("string"!=typeof e)return 0===e?e:+e;e=e.replace(r,"");var n=s.test(e);return n||a.test(e)?u(e.slice(2),n?2:8):c.test(e)?o:+e}e.exports=function(e,t,o){var i=!0,r=!0;if("function"!=typeof e)throw new TypeError(n);return y(o)&&(i="leading"in o?!!o.leading:i,r="trailing"in o?!!o.trailing:r),v(e,t,{leading:i,maxWait:t,trailing:r})}}).call(this,n(1))}]); \ No newline at end of file +!function(e){var t={};function n(o){if(t[o])return t[o].exports;var i=t[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(o,i,function(t){return e[t]}.bind(null,i));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=3)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});let o=void 0;function i(e){const t=document.getElementById("vscode-markdown-preview-data");if(t){const n=t.getAttribute(e);if(n)return JSON.parse(n)}throw new Error(`Could not load data for ${e}`)}t.getData=i,t.getSettings=function(){if(o)return o;if(o=i("data-settings"))return o;throw new Error("Could not load settings")}},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const o=n(0),i="code-line";function r(e){return t=0,n=o.getSettings().lineCount-1,i=e,Math.min(n,Math.max(t,i));var t,n,i}const c=(()=>{let e;return()=>{if(!e){e=[{element:document.body,line:0}];for(const t of document.getElementsByClassName(i)){const n=+t.getAttribute("data-line");isNaN(n)||("CODE"===t.tagName&&t.parentElement&&"PRE"===t.parentElement.tagName?e.push({element:t.parentElement,line:n}):e.push({element:t,line:n}))}}return e}})();function s(e){const t=Math.floor(e),n=c();let o=n[0]||null;for(const e of n){if(e.line===t)return{previous:e,next:void 0};if(e.line>t)return{previous:o,next:e};o=e}return{previous:o}}function a(e){const t=c(),n=e-window.scrollY;let o=-1,i=t.length-1;for(;o+1=n?i=e:o=e}const r=t[i],s=u(r);if(i>=1&&s.top>n){return{previous:t[o],next:r}}return i>1&&in?{previous:r,next:t[i+1]}:{previous:r}}function u({element:e}){const t=e.getBoundingClientRect(),n=e.querySelector(`.${i}`);if(n){const e=n.getBoundingClientRect(),o=Math.max(1,e.top-t.top);return{top:t.top,height:o}}return t}t.getElementsForSourceLine=s,t.getLineElementsAtPageOffset=a,t.scrollToRevealSourceLine=function(e){if(!o.getSettings().scrollPreviewWithEditor)return;if(e<=0)return void window.scroll(window.scrollX,0);const{previous:t,next:n}=s(e);if(!t)return;let i=0;const r=u(t),c=r.top;if(n&&n.line!==t.line){i=c+(e-t.line)/(n.line-t.line)*(n.element.getBoundingClientRect().top-c)}else{const t=e-Math.floor(e);i=c+r.height*t}window.scroll(window.scrollX,Math.max(1,window.scrollY+i))},t.getEditorLineNumberForPageOffset=function(e){const{previous:t,next:n}=a(e);if(t){const o=u(t),i=e-window.scrollY-o.top;if(n){const e=i/(u(n).top-o.top);return r(t.line+e*(n.line-t.line))}{const e=i/o.height;return r(t.line+e)}}return null},t.getLineElementForFragment=function(e){return c().find(t=>t.element.id===e)}},function(e,t,n){"use strict";(function(e){Object.defineProperty(t,"__esModule",{value:!0});const o=n(7),i=n(8),r=n(9),c=n(2),s=n(0),a=n(10);let u=!0;const l=new o.ActiveLineMarker,f=s.getSettings(),d=acquireVsCodeApi(),m=d.getState(),p={..."object"==typeof m?m:{},...s.getData("data-state")};d.setState(p);const g=r.createPosterForVsCode(d);window.cspAlerter.setPoster(g),window.styleLoadingMonitor.setPoster(g),window.onload=()=>{v()},i.onceDocumentLoaded(()=>{const t=p.scrollProgress;"number"!=typeof t||f.fragment?f.scrollPreviewWithEditor&&e(()=>{if(f.fragment){p.fragment=void 0,d.setState(p);const e=c.getLineElementForFragment(f.fragment);e&&(u=!0,c.scrollToRevealSourceLine(e.line))}else isNaN(f.line)||(u=!0,c.scrollToRevealSourceLine(f.line))}):e(()=>{u=!0,window.scrollTo(0,t*document.body.clientHeight)})});const h=(()=>{const e=a(e=>{u=!0,c.scrollToRevealSourceLine(e)},50);return t=>{isNaN(t)||(p.line=t,e(t))}})();let v=a(()=>{const e=[];let t=document.getElementsByTagName("img");if(t){let n;for(n=0;n{u=!0,w(),v()},!0),window.addEventListener("message",e=>{if(e.data.source===f.source)switch(e.data.type){case"onDidChangeTextEditorSelection":l.onDidChangeTextEditorSelection(e.data.line);break;case"updateView":h(e.data.line)}},!1),document.addEventListener("dblclick",e=>{if(!f.doubleClickToSwitchToEditor)return;for(let t=e.target;t;t=t.parentNode)if("A"===t.tagName)return;const t=e.pageY,n=c.getEditorLineNumberForPageOffset(t);"number"!=typeof n||isNaN(n)||g.postMessage("didClick",{line:Math.floor(n)})});const y=["http:","https:","mailto:","vscode:","vscode-insiders:"];function w(){p.scrollProgress=window.scrollY/document.body.clientHeight,d.setState(p)}document.addEventListener("click",e=>{if(!e)return;let t=e.target;for(;t;){if(t.tagName&&"A"===t.tagName&&t.href){if(t.getAttribute("href").startsWith("#"))return;let n=t.getAttribute("data-href");if(!n){if(y.some(e=>t.href.startsWith(e)))return;n=t.getAttribute("href")}return/^[a-z\-]+:/i.test(n)?void 0:(g.postMessage("openLink",{href:n}),e.preventDefault(),void e.stopPropagation())}t=t.parentNode}},!0),window.addEventListener("scroll",a(()=>{if(w(),u)u=!1;else{const e=c.getEditorLineNumberForPageOffset(window.scrollY);"number"!=typeof e||isNaN(e)||g.postMessage("revealLine",{line:e})}},50))}).call(this,n(4).setImmediate)},function(e,t,n){(function(e){var o=Function.prototype.apply;function i(e,t){this._id=e,this._clearFn=t}t.setTimeout=function(){return new i(o.call(setTimeout,window,arguments),clearTimeout)},t.setInterval=function(){return new i(o.call(setInterval,window,arguments),clearInterval)},t.clearTimeout=t.clearInterval=function(e){e&&e.close()},i.prototype.unref=i.prototype.ref=function(){},i.prototype.close=function(){this._clearFn.call(window,this._id)},t.enroll=function(e,t){clearTimeout(e._idleTimeoutId),e._idleTimeout=t},t.unenroll=function(e){clearTimeout(e._idleTimeoutId),e._idleTimeout=-1},t._unrefActive=t.active=function(e){clearTimeout(e._idleTimeoutId);var t=e._idleTimeout;t>=0&&(e._idleTimeoutId=setTimeout((function(){e._onTimeout&&e._onTimeout()}),t))},n(5),t.setImmediate="undefined"!=typeof self&&self.setImmediate||void 0!==e&&e.setImmediate||this&&this.setImmediate,t.clearImmediate="undefined"!=typeof self&&self.clearImmediate||void 0!==e&&e.clearImmediate||this&&this.clearImmediate}).call(this,n(1))},function(e,t,n){(function(e,t){!function(e,n){"use strict";if(!e.setImmediate){var o,i,r,c,s,a=1,u={},l=!1,f=e.document,d=Object.getPrototypeOf&&Object.getPrototypeOf(e);d=d&&d.setTimeout?d:e,"[object process]"==={}.toString.call(e.process)?o=function(e){t.nextTick((function(){p(e)}))}:!function(){if(e.postMessage&&!e.importScripts){var t=!0,n=e.onmessage;return e.onmessage=function(){t=!1},e.postMessage("","*"),e.onmessage=n,t}}()?e.MessageChannel?((r=new MessageChannel).port1.onmessage=function(e){p(e.data)},o=function(e){r.port2.postMessage(e)}):f&&"onreadystatechange"in f.createElement("script")?(i=f.documentElement,o=function(e){var t=f.createElement("script");t.onreadystatechange=function(){p(e),t.onreadystatechange=null,i.removeChild(t),t=null},i.appendChild(t)}):o=function(e){setTimeout(p,0,e)}:(c="setImmediate$"+Math.random()+"$",s=function(t){t.source===e&&"string"==typeof t.data&&0===t.data.indexOf(c)&&p(+t.data.slice(c.length))},e.addEventListener?e.addEventListener("message",s,!1):e.attachEvent("onmessage",s),o=function(t){e.postMessage(c+t,"*")}),d.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),n=0;n1)for(var n=1;nnew class{postMessage(t,n){e.postMessage({type:t,source:o.getSettings().source,body:n})}}},function(e,t,n){(function(t){var n="Expected a function",o=NaN,i="[object Symbol]",r=/^\s+|\s+$/g,c=/^[-+]0x[0-9a-f]+$/i,s=/^0b[01]+$/i,a=/^0o[0-7]+$/i,u=parseInt,l="object"==typeof t&&t&&t.Object===Object&&t,f="object"==typeof self&&self&&self.Object===Object&&self,d=l||f||Function("return this")(),m=Object.prototype.toString,p=Math.max,g=Math.min,h=function(){return d.Date.now()};function v(e,t,o){var i,r,c,s,a,u,l=0,f=!1,d=!1,m=!0;if("function"!=typeof e)throw new TypeError(n);function v(t){var n=i,o=r;return i=r=void 0,l=t,s=e.apply(o,n)}function b(e){var n=e-u;return void 0===u||n>=t||n<0||d&&e-l>=c}function T(){var e=h();if(b(e))return E(e);a=setTimeout(T,function(e){var n=t-(e-u);return d?g(n,c-(e-l)):n}(e))}function E(e){return a=void 0,m&&i?v(e):(i=r=void 0,s)}function _(){var e=h(),n=b(e);if(i=arguments,r=this,u=e,n){if(void 0===a)return function(e){return l=e,a=setTimeout(T,t),f?v(e):s}(u);if(d)return a=setTimeout(T,t),v(u)}return void 0===a&&(a=setTimeout(T,t)),s}return t=w(t)||0,y(o)&&(f=!!o.leading,c=(d="maxWait"in o)?p(w(o.maxWait)||0,t):c,m="trailing"in o?!!o.trailing:m),_.cancel=function(){void 0!==a&&clearTimeout(a),l=0,i=u=r=a=void 0},_.flush=function(){return void 0===a?s:E(h())},_}function y(e){var t=typeof e;return!!e&&("object"==t||"function"==t)}function w(e){if("number"==typeof e)return e;if(function(e){return"symbol"==typeof e||function(e){return!!e&&"object"==typeof e}(e)&&m.call(e)==i}(e))return o;if(y(e)){var t="function"==typeof e.valueOf?e.valueOf():e;e=y(t)?t+"":t}if("string"!=typeof e)return 0===e?e:+e;e=e.replace(r,"");var n=s.test(e);return n||a.test(e)?u(e.slice(2),n?2:8):c.test(e)?o:+e}e.exports=function(e,t,o){var i=!0,r=!0;if("function"!=typeof e)throw new TypeError(n);return y(o)&&(i="leading"in o?!!o.leading:i,r="trailing"in o?!!o.trailing:r),v(e,t,{leading:i,maxWait:t,trailing:r})}}).call(this,n(1))}]); \ No newline at end of file diff --git a/extensions/markdown-language-features/media/markdown.css b/extensions/markdown-language-features/media/markdown.css index fbab6086d2d..f581cd00252 100644 --- a/extensions/markdown-language-features/media/markdown.css +++ b/extensions/markdown-language-features/media/markdown.css @@ -128,21 +128,12 @@ textarea:focus { } p { - margin-bottom: 1.5em; -} - -li > p { - margin-bottom: 0; -} - -/* don't space 2 paragraphs too far apart */ -p + p { - margin-top: -0.8em; + margin-bottom: 0.7em; } ul, ol { - margin-bottom: 1.5em; + margin-bottom: 0.7em; } hr { diff --git a/extensions/markdown-language-features/preview-src/index.ts b/extensions/markdown-language-features/preview-src/index.ts index 4571699f762..064c30ac969 100644 --- a/extensions/markdown-language-features/preview-src/index.ts +++ b/extensions/markdown-language-features/preview-src/index.ts @@ -163,13 +163,15 @@ document.addEventListener('click', event => { return; } - // Pass through known schemes - if (passThroughLinkSchemes.some(scheme => node.href.startsWith(scheme))) { - return; + let hrefText = node.getAttribute('data-href'); + if (!hrefText) { + // Pass through known schemes + if (passThroughLinkSchemes.some(scheme => node.href.startsWith(scheme))) { + return; + } + hrefText = node.getAttribute('href'); } - const hrefText = node.getAttribute('data-href') || node.getAttribute('href'); - // If original link doesn't look like a url, delegate back to VS Code to resolve if (!/^[a-z\-]+:/i.test(hrefText)) { messaging.postMessage('openLink', { href: hrefText }); diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index 06a71068c2e..8bb509d4565 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -129,7 +129,6 @@ export class MarkdownEngine { } this.currentDocument = document.uri; - this._slugCount = new Map(); const tokens = this.tokenizeString(document.getText(), engine); this._tokenCache.update(document, config, tokens); @@ -137,6 +136,8 @@ export class MarkdownEngine { } private tokenizeString(text: string, engine: MarkdownIt) { + this._slugCount = new Map(); + return engine.parse(text.replace(UNICODE_NEWLINE_REGEX, ''), {}); } @@ -355,4 +356,3 @@ function normalizeHighlightLang(lang: string | undefined) { return lang; } } - diff --git a/extensions/markdown-language-features/yarn.lock b/extensions/markdown-language-features/yarn.lock index 23cd1744005..008969d899e 100644 --- a/extensions/markdown-language-features/yarn.lock +++ b/extensions/markdown-language-features/yarn.lock @@ -538,9 +538,9 @@ bluebird@^3.5.5: integrity sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg== bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: - version "4.11.8" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" - integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== + version "4.11.9" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" + integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== boom@2.x.x: version "2.10.1" @@ -1265,9 +1265,9 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" elliptic@^6.0.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" - integrity sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8= + version "6.5.3" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" + integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== dependencies: bn.js "^4.4.0" brorand "^1.0.1" @@ -2099,12 +2099,12 @@ hash-base@^3.0.0: safe-buffer "^5.0.1" hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846" - integrity sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA== + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== dependencies: inherits "^2.0.3" - minimalistic-assert "^1.0.0" + minimalistic-assert "^1.0.1" hawk@3.1.3, hawk@~3.1.3: version "3.1.3" @@ -2221,16 +2221,21 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== inherits@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" @@ -2773,9 +2778,9 @@ lodash.throttle@^4.1.1: integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= lodash@^4.16.4: - version "4.17.10" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" - integrity sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg== + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== lru-cache@^5.1.1: version "5.1.1" @@ -2981,10 +2986,10 @@ mimic-fn@^2.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -minimalistic-assert@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" - integrity sha1-cCvi3aazf0g2vLP121ZkG2Sh09M= +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: version "1.0.1" diff --git a/extensions/merge-conflict/.vscodeignore b/extensions/merge-conflict/.vscodeignore index 36e8b0714fa..f071cfb7c71 100644 --- a/extensions/merge-conflict/.vscodeignore +++ b/extensions/merge-conflict/.vscodeignore @@ -2,4 +2,5 @@ src/** tsconfig.json out/** extension.webpack.config.js -yarn.lock \ No newline at end of file +extension-browser.webpack.config.js +yarn.lock diff --git a/extensions/microsoft-authentication/.vscodeignore b/extensions/microsoft-authentication/.vscodeignore index ed3f9d37c1f..46f23a20dba 100644 --- a/extensions/microsoft-authentication/.vscodeignore +++ b/extensions/microsoft-authentication/.vscodeignore @@ -1,10 +1,14 @@ .vscode/** .vscode-test/** out/test/** +out/** +extension.webpack.config.js +extension-browser.webpack.config.js +yarn.lock src/** .gitignore vsc-extension-quickstart.md **/tsconfig.json **/tslint.json **/*.map -**/*.ts \ No newline at end of file +**/*.ts diff --git a/extensions/microsoft-authentication/extension-browser.webpack.config.js b/extensions/microsoft-authentication/extension-browser.webpack.config.js index 5d4b88ed221..d2f4d9ee94b 100644 --- a/extensions/microsoft-authentication/extension-browser.webpack.config.js +++ b/extensions/microsoft-authentication/extension-browser.webpack.config.js @@ -22,6 +22,7 @@ module.exports = withBrowserDefaults({ resolve: { alias: { './env/node': path.resolve(__dirname, 'src/env/browser'), + './authServer': path.resolve(__dirname, 'src/env/browser/authServer'), 'buffer': path.resolve(__dirname, 'node_modules/buffer/index.js'), 'node-fetch': path.resolve(__dirname, 'node_modules/node-fetch/browser.js'), 'randombytes': path.resolve(__dirname, 'node_modules/randombytes/browser.js'), diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 4300598429d..81ed5c32e4f 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -12,7 +12,13 @@ ], "enableProposedApi": true, "activationEvents": [ - "*" + "*", + "onAuthenticationRequest:microsoft" + ], + "extensionKind": [ + "ui", + "workspace", + "web" ], "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", "main": "./out/extension.js", diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index b553b1a64ca..035c5af7350 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -6,7 +6,7 @@ import * as randomBytes from 'randombytes'; import * as querystring from 'querystring'; import * as vscode from 'vscode'; -import { createServer, startServer } from './env/node/authServer'; +import { createServer, startServer } from './authServer'; import { v4 as uuid } from 'uuid'; import { keychain } from './keychain'; @@ -73,7 +73,7 @@ function parseQuery(uri: vscode.Uri) { }, {}); } -export const onDidChangeSessions = new vscode.EventEmitter(); +export const onDidChangeSessions = new vscode.EventEmitter(); export const REFRESH_NETWORK_FAILURE = 'Network failure'; @@ -339,7 +339,7 @@ export class AzureActiveDirectoryService { } private getCallbackEnvironment(callbackUri: vscode.Uri): string { - if (callbackUri.authority.endsWith('.workspaces.github.com')) { + if (callbackUri.authority.endsWith('.workspaces.github.com') || callbackUri.authority.endsWith('.github.dev')) { return `${callbackUri.authority},`; } @@ -471,7 +471,10 @@ export class AzureActiveDirectoryService { redirect_uri: redirectUrl }); - const result = await fetch(`${loginEndpointUrl}${tenant}/oauth2/v2.0/token`, { + const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints'); + const endpoint = proxyEndpoints && proxyEndpoints['microsoft'] || `${loginEndpointUrl}${tenant}/oauth2/v2.0/token`; + + const result = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', diff --git a/extensions/microsoft-authentication/src/env/node/authServer.ts b/extensions/microsoft-authentication/src/authServer.ts similarity index 95% rename from extensions/microsoft-authentication/src/env/node/authServer.ts rename to extensions/microsoft-authentication/src/authServer.ts index 88e02015fd2..caae72ba52c 100644 --- a/extensions/microsoft-authentication/src/env/node/authServer.ts +++ b/extensions/microsoft-authentication/src/authServer.ts @@ -130,10 +130,10 @@ export function createServer(nonce: string) { } break; case '/': - sendFile(res, path.join(__dirname, '../../../media/auth.html'), 'text/html; charset=utf-8'); + sendFile(res, path.join(__dirname, '../media/auth.html'), 'text/html; charset=utf-8'); break; case '/auth.css': - sendFile(res, path.join(__dirname, '../../../media/auth.css'), 'text/css; charset=utf-8'); + sendFile(res, path.join(__dirname, '../media/auth.css'), 'text/css; charset=utf-8'); break; case '/callback': deferredCode.resolve(callback(nonce, reqUrl) diff --git a/extensions/npm/.vscodeignore b/extensions/npm/.vscodeignore index 27bb76ffd24..7700b94ebb0 100644 --- a/extensions/npm/.vscodeignore +++ b/extensions/npm/.vscodeignore @@ -3,4 +3,5 @@ out/** tsconfig.json .vscode/** extension.webpack.config.js -yarn.lock \ No newline at end of file +extension-browser.webpack.config.js +yarn.lock diff --git a/extensions/npm/package.json b/extensions/npm/package.json index 85ef490df23..a77b48647cd 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -40,13 +40,13 @@ "languages": [ { "id": "ignore", - "filenames": [ + "extensions": [ ".npmignore" ] }, { "id": "properties", - "filenames": [ + "extensions": [ ".npmrc" ] } diff --git a/extensions/package.json b/extensions/package.json index 7c668c9744a..665553eeeae 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -3,12 +3,9 @@ "version": "0.0.1", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "3.9.6" + "typescript": "3.9.7" }, "scripts": { "postinstall": "node ./postinstall" - }, - "devDependencies": { - "rimraf": "^3.0.2" } } diff --git a/extensions/python/.vscodeignore b/extensions/python/.vscodeignore index 4d5a14fc91e..b5c95d0fb64 100644 --- a/extensions/python/.vscodeignore +++ b/extensions/python/.vscodeignore @@ -1,6 +1,8 @@ test/** src/** +out/** tsconfig.json extension.webpack.config.js +extension-browser.webpack.config.js cgmanifest.json -.vscode \ No newline at end of file +.vscode diff --git a/extensions/python/package.json b/extensions/python/package.json index 612fcf76504..e7c75aa4ea1 100644 --- a/extensions/python/package.json +++ b/extensions/python/package.json @@ -9,7 +9,7 @@ "activationEvents": ["onLanguage:python"], "main": "./out/pythonMain", "browser": "./dist/browser/pythonMain", - "extensionKind": [ "ui", "workspace" ], + "extensionKind": [ "ui", "workspace", "web" ], "contributes": { "languages": [{ "id": "python", diff --git a/extensions/github-browser/.vscodeignore b/extensions/search-result/.vscodeignore similarity index 67% rename from extensions/github-browser/.vscodeignore rename to extensions/search-result/.vscodeignore index 32fe3f03697..da3d2763686 100644 --- a/extensions/github-browser/.vscodeignore +++ b/extensions/search-result/.vscodeignore @@ -1,11 +1,6 @@ -.vscode/** -build/** -dist/** -out/** src/** -typings/** -.gitignore -extension-browser.webpack.config.js -extension.webpack.config.js +out/** tsconfig.json +extension.webpack.config.js +extension-browser.webpack.config.js yarn.lock diff --git a/extensions/github-browser/extension-browser.webpack.config.js b/extensions/search-result/extension-browser.webpack.config.js similarity index 75% rename from extensions/github-browser/extension-browser.webpack.config.js rename to extensions/search-result/extension-browser.webpack.config.js index 55f3a268486..10c0a19e356 100644 --- a/extensions/github-browser/extension-browser.webpack.config.js +++ b/extensions/search-result/extension-browser.webpack.config.js @@ -6,20 +6,17 @@ //@ts-check 'use strict'; -const path = require('path'); -const withBrowserDefaults = require('../shared.webpack.config').browser; -const config = withBrowserDefaults({ +const withBrowserDefaults = require('../shared.webpack.config').browser; +const path = require('path'); + +module.exports = withBrowserDefaults({ context: __dirname, - node: false, entry: { extension: './src/extension.ts' }, - resolve: { - alias: { - 'node-fetch': path.resolve(__dirname, 'node_modules/node-fetch/browser.js') - } + output: { + filename: 'extension.js', + path: path.join(__dirname, 'dist') } }); - -module.exports = config; diff --git a/extensions/search-result/package.json b/extensions/search-result/package.json index ffb5321ae4d..7b43da243cf 100644 --- a/extensions/search-result/package.json +++ b/extensions/search-result/package.json @@ -12,6 +12,7 @@ "Programming Languages" ], "main": "./out/extension.js", + "browser": "./dist/extension.js", "activationEvents": [ "*" ], diff --git a/extensions/search-result/src/extension.ts b/extensions/search-result/src/extension.ts index abb85dac201..3abaa97de56 100644 --- a/extensions/search-result/src/extension.ts +++ b/extensions/search-result/src/extension.ts @@ -126,6 +126,8 @@ function relativePathToUri(path: string, resultsUri: vscode.Uri): vscode.Uri | u return vscode.Uri.file(pathUtils.join(process.env.HOME!, path.slice(2))); } + const uriFromFolderWithPath = (folder: vscode.WorkspaceFolder, path: string): vscode.Uri => + folder.uri.with({ path: pathUtils.join(folder.uri.fsPath, path) }); if (vscode.workspace.workspaceFolders) { const multiRootFormattedPath = /^(.*) • (.*)$/.exec(path); @@ -133,17 +135,18 @@ function relativePathToUri(path: string, resultsUri: vscode.Uri): vscode.Uri | u const [, workspaceName, workspacePath] = multiRootFormattedPath; const folder = vscode.workspace.workspaceFolders.filter(wf => wf.name === workspaceName)[0]; if (folder) { - return vscode.Uri.file(pathUtils.join(folder.uri.fsPath, workspacePath)); + return uriFromFolderWithPath(folder, workspacePath); } } - else if (vscode.workspace.workspaceFolders.length === 1) { - return vscode.Uri.file(pathUtils.join(vscode.workspace.workspaceFolders[0].uri.fsPath, path)); + return uriFromFolderWithPath(vscode.workspace.workspaceFolders[0], path); } else if (resultsUri.scheme !== 'untitled') { // We're in a multi-root workspace, but the path is not multi-root formatted // Possibly a saved search from a single root session. Try checking if the search result document's URI is in a current workspace folder. const prefixMatch = vscode.workspace.workspaceFolders.filter(wf => resultsUri.toString().startsWith(wf.uri.toString()))[0]; - if (prefixMatch) { return vscode.Uri.file(pathUtils.join(prefixMatch.uri.fsPath, path)); } + if (prefixMatch) { + return uriFromFolderWithPath(prefixMatch, path); + } } } diff --git a/extensions/shared.webpack.config.js b/extensions/shared.webpack.config.js index 3548671829f..ab6a40c6b80 100644 --- a/extensions/shared.webpack.config.js +++ b/extensions/shared.webpack.config.js @@ -78,7 +78,7 @@ function withNodeDefaults(/**@type WebpackConfig*/extConfig) { }; return merge(defaultConfig, extConfig); -}; +} function withBrowserDefaults(/**@type WebpackConfig*/extConfig) { @@ -135,7 +135,7 @@ function withBrowserDefaults(/**@type WebpackConfig*/extConfig) { }; return merge(defaultConfig, extConfig); -}; +} module.exports = withNodeDefaults; diff --git a/extensions/typescript-basics/.vscodeignore b/extensions/typescript-basics/.vscodeignore index 06c7b11c5e6..0a0a50bc3e0 100644 --- a/extensions/typescript-basics/.vscodeignore +++ b/extensions/typescript-basics/.vscodeignore @@ -3,3 +3,4 @@ src/** test/** tsconfig.json cgmanifest.json +syntaxes/Readme.md diff --git a/extensions/typescript-basics/package.json b/extensions/typescript-basics/package.json index 6b8c281538e..e3120789cf6 100644 --- a/extensions/typescript-basics/package.json +++ b/extensions/typescript-basics/package.json @@ -45,7 +45,9 @@ ], "filenamePatterns": [ "tsconfig.*.json", - "tsconfig-*.json" + "jsconfig.*.json", + "tsconfig-*.json", + "jsconfig-*.json" ] } ], diff --git a/extensions/typescript-language-features/.vscodeignore b/extensions/typescript-language-features/.vscodeignore index 1edbc2a7b5e..079f06f08d9 100644 --- a/extensions/typescript-language-features/.vscodeignore +++ b/extensions/typescript-language-features/.vscodeignore @@ -1,8 +1,10 @@ build/** src/** test/** +test-workspace/** out/** tsconfig.json extension.webpack.config.js +extension-browser.webpack.config.js cgmanifest.json yarn.lock diff --git a/extensions/typescript-language-features/cgmanifest.json b/extensions/typescript-language-features/cgmanifest.json index 11b737dc59a..3a011a77078 100644 --- a/extensions/typescript-language-features/cgmanifest.json +++ b/extensions/typescript-language-features/cgmanifest.json @@ -28,7 +28,7 @@ "type": "other", "other": { "name": "Unicode", - "downloadUrl": "http://www.unicode.org/", + "downloadUrl": "https://home.unicode.org/", "version": "12.0.0" } }, diff --git a/extensions/typescript-language-features/extension-browser.webpack.config.js b/extensions/typescript-language-features/extension-browser.webpack.config.js new file mode 100644 index 00000000000..fd8e5877c3a --- /dev/null +++ b/extensions/typescript-language-features/extension-browser.webpack.config.js @@ -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. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; +const CopyPlugin = require('copy-webpack-plugin'); +const { lchmod } = require('graceful-fs'); +const Terser = require('terser'); + +const withBrowserDefaults = require('../shared.webpack.config').browser; + +module.exports = withBrowserDefaults({ + context: __dirname, + entry: { + extension: './src/extension.browser.ts', + }, + plugins: [ + // @ts-ignore + new CopyPlugin({ + patterns: [ + { + from: 'node_modules/typescript-web-server/*.d.ts', + to: 'typescript-web/', + flatten: true + }, + ], + }), + // @ts-ignore + new CopyPlugin({ + patterns: [ + { + from: 'node_modules/typescript-web-server/tsserver.js', + to: 'typescript-web/tsserver.web.js', + transform: (content) => { + return Terser.minify(content.toString()).code; + + }, + transformPath: (targetPath) => { + return targetPath.replace('tsserver.js', 'tsserver.web.js'); + } + } + ], + }), + ], +}); diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 23c4a4004d9..70ed27503cd 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -27,10 +27,15 @@ "@types/node": "^12.11.7", "@types/rimraf": "2.0.2", "@types/semver": "^5.5.0", + "copy-webpack-plugin": "^6.0.3", + "terser": "^4.8.0", + "typescript-web-server": "git://github.com/mjbvz/ts-server-web-build", "vscode": "^1.1.36" }, "scripts": { - "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:typescript-language-features" + "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:typescript-language-features", + "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", + "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "activationEvents": [ "onLanguage:javascript", @@ -50,6 +55,7 @@ "onLanguage:jsonc" ], "main": "./out/extension", + "browser": "./dist/browser/extension", "contributes": { "jsonValidation": [ { @@ -680,6 +686,22 @@ "description": "%typescript.preferences.importModuleSpecifierEnding%", "scope": "resource" }, + "typescript.preferences.includePackageJsonAutoImports": { + "type": "string", + "enum": [ + "auto", + "on", + "off" + ], + "enumDescriptions": [ + "%typescript.preferences.includePackageJsonAutoImports.auto%", + "%typescript.preferences.includePackageJsonAutoImports.on%", + "%typescript.preferences.includePackageJsonAutoImports.off%" + ], + "default": "auto", + "markdownDescription": "%typescript.preferences.includePackageJsonAutoImports%", + "scope": "window" + }, "javascript.preferences.renameShorthandProperties": { "type": "boolean", "default": true, @@ -1008,10 +1030,10 @@ "background": { "activeOnStart": true, "beginsPattern": { - "regexp": "^\\s*(?:message TS6032:|\\[?\\D*\\d{1,2}[:.]\\d{1,2}[:.]\\d{1,2}\\D*(?:\\]| -)) File change detected\\. Starting incremental compilation\\.\\.\\." + "regexp": "^\\s*(?:message TS6032:|\\[?\\D*\\d{1,2}[:.]\\d{1,2}[:.]\\d{1,2}\\D*(ā”œ\\D*\\d{1,2}\\D+┤)?(?:\\]| -)) File change detected\\. Starting incremental compilation\\.\\.\\." }, "endsPattern": { - "regexp": "^\\s*(?:message TS6042:|\\[?\\D*\\d{1,2}[:.]\\d{1,2}[:.]\\d{1,2}\\D*(?:\\]| -)) (?:Compilation complete\\.|Found \\d+ errors?\\.) Watching for file changes\\." + "regexp": "^\\s*(?:message TS6042:|\\[?\\D*\\d{1,2}[:.]\\d{1,2}[:.]\\d{1,2}\\D*(ā”œ\\D*\\d{1,2}\\D+┤)?(?:\\]| -)) (?:Compilation complete\\.|Found \\d+ errors?\\.) Watching for file changes\\." } } } diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 6f4241434ef..4322984e099 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -74,8 +74,12 @@ "typescript.preferences.importModuleSpecifierEnding": "Preferred path ending for auto imports.", "typescript.preferences.importModuleSpecifierEnding.auto": "Use project settings to select a default.", "typescript.preferences.importModuleSpecifierEnding.minimal": "Shorten `./component/index.js` to `./component`.", - "typescript.preferences.importModuleSpecifierEnding.index": "Shorten `./component/index.js` to `./component/index`", + "typescript.preferences.importModuleSpecifierEnding.index": "Shorten `./component/index.js` to `./component/index`.", "typescript.preferences.importModuleSpecifierEnding.js": "Do not shorten path endings; include the `.js` extension.", + "typescript.preferences.includePackageJsonAutoImports": "Enable/disable searching `package.json` dependencies for available auto imports.", + "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.prompt": "Prompt on each rename.", "typescript.updateImportsOnFileMove.enabled.always": "Always update paths automatically.", diff --git a/extensions/typescript-language-features/src/utils/commandManager.ts b/extensions/typescript-language-features/src/commands/commandManager.ts similarity index 100% rename from extensions/typescript-language-features/src/utils/commandManager.ts rename to extensions/typescript-language-features/src/commands/commandManager.ts diff --git a/extensions/typescript-language-features/src/commands/configurePlugin.ts b/extensions/typescript-language-features/src/commands/configurePlugin.ts index 8af85d8b94e..f781c4a50fa 100644 --- a/extensions/typescript-language-features/src/commands/configurePlugin.ts +++ b/extensions/typescript-language-features/src/commands/configurePlugin.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Command } from '../utils/commandManager'; import { PluginManager } from '../utils/plugins'; +import { Command } from './commandManager'; export class ConfigurePluginCommand implements Command { public readonly id = '_typescript.configurePlugin'; diff --git a/extensions/typescript-language-features/src/commands/goToProjectConfiguration.ts b/extensions/typescript-language-features/src/commands/goToProjectConfiguration.ts index a4fb9c33b4e..11adec35251 100644 --- a/extensions/typescript-language-features/src/commands/goToProjectConfiguration.ts +++ b/extensions/typescript-language-features/src/commands/goToProjectConfiguration.ts @@ -5,9 +5,9 @@ import * as vscode from 'vscode'; import TypeScriptServiceClientHost from '../typeScriptServiceClientHost'; -import { Command } from '../utils/commandManager'; import { Lazy } from '../utils/lazy'; import { openProjectConfigForFile, ProjectType } from '../utils/tsconfig'; +import { Command } from './commandManager'; export class TypeScriptGoToProjectConfigCommand implements Command { public readonly id = 'typescript.goToProjectConfig'; diff --git a/extensions/typescript-language-features/src/commands/index.ts b/extensions/typescript-language-features/src/commands/index.ts index 8e4cfddcccd..7ab7ae09d20 100644 --- a/extensions/typescript-language-features/src/commands/index.ts +++ b/extensions/typescript-language-features/src/commands/index.ts @@ -4,22 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import TypeScriptServiceClientHost from '../typeScriptServiceClientHost'; -import { CommandManager } from '../utils/commandManager'; import { Lazy } from '../utils/lazy'; import { PluginManager } from '../utils/plugins'; +import { CommandManager } from './commandManager'; import { ConfigurePluginCommand } from './configurePlugin'; import { JavaScriptGoToProjectConfigCommand, TypeScriptGoToProjectConfigCommand } from './goToProjectConfiguration'; +import { LearnMoreAboutRefactoringsCommand } from './learnMoreAboutRefactorings'; import { OpenTsServerLogCommand } from './openTsServerLog'; import { ReloadJavaScriptProjectsCommand, ReloadTypeScriptProjectsCommand } from './reloadProject'; import { RestartTsServerCommand } from './restartTsServer'; import { SelectTypeScriptVersionCommand } from './selectTypeScriptVersion'; -import { LearnMoreAboutRefactoringsCommand } from './learnMoreAboutRefactorings'; -export function registerCommands( +export function registerBaseCommands( commandManager: CommandManager, lazyClientHost: Lazy, pluginManager: PluginManager -) { +): void { commandManager.register(new ReloadTypeScriptProjectsCommand(lazyClientHost)); commandManager.register(new ReloadJavaScriptProjectsCommand(lazyClientHost)); commandManager.register(new SelectTypeScriptVersionCommand(lazyClientHost)); diff --git a/extensions/typescript-language-features/src/commands/learnMoreAboutRefactorings.ts b/extensions/typescript-language-features/src/commands/learnMoreAboutRefactorings.ts index 21366d6c607..b166397e38b 100644 --- a/extensions/typescript-language-features/src/commands/learnMoreAboutRefactorings.ts +++ b/extensions/typescript-language-features/src/commands/learnMoreAboutRefactorings.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { Command } from '../utils/commandManager'; import { isTypeScriptDocument } from '../utils/languageModeIds'; +import { Command } from './commandManager'; export class LearnMoreAboutRefactoringsCommand implements Command { public static readonly id = '_typescript.learnMoreAboutRefactorings'; diff --git a/extensions/typescript-language-features/src/commands/openTsServerLog.ts b/extensions/typescript-language-features/src/commands/openTsServerLog.ts index be47a985cd7..cd41445fef2 100644 --- a/extensions/typescript-language-features/src/commands/openTsServerLog.ts +++ b/extensions/typescript-language-features/src/commands/openTsServerLog.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import TypeScriptServiceClientHost from '../typeScriptServiceClientHost'; -import { Command } from '../utils/commandManager'; import { Lazy } from '../utils/lazy'; +import { Command } from './commandManager'; export class OpenTsServerLogCommand implements Command { public readonly id = 'typescript.openTsServerLog'; diff --git a/extensions/typescript-language-features/src/commands/reloadProject.ts b/extensions/typescript-language-features/src/commands/reloadProject.ts index fbba68eb94e..4da59685f67 100644 --- a/extensions/typescript-language-features/src/commands/reloadProject.ts +++ b/extensions/typescript-language-features/src/commands/reloadProject.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import TypeScriptServiceClientHost from '../typeScriptServiceClientHost'; -import { Command } from '../utils/commandManager'; import { Lazy } from '../utils/lazy'; +import { Command } from './commandManager'; export class ReloadTypeScriptProjectsCommand implements Command { public readonly id = 'typescript.reloadProjects'; diff --git a/extensions/typescript-language-features/src/commands/restartTsServer.ts b/extensions/typescript-language-features/src/commands/restartTsServer.ts index a357224d352..77dcae870ee 100644 --- a/extensions/typescript-language-features/src/commands/restartTsServer.ts +++ b/extensions/typescript-language-features/src/commands/restartTsServer.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import TypeScriptServiceClientHost from '../typeScriptServiceClientHost'; -import { Command } from '../utils/commandManager'; import { Lazy } from '../utils/lazy'; +import { Command } from './commandManager'; export class RestartTsServerCommand implements Command { public readonly id = 'typescript.restartTsServer'; diff --git a/extensions/typescript-language-features/src/commands/selectTypeScriptVersion.ts b/extensions/typescript-language-features/src/commands/selectTypeScriptVersion.ts index f375b55e938..d70f59472ff 100644 --- a/extensions/typescript-language-features/src/commands/selectTypeScriptVersion.ts +++ b/extensions/typescript-language-features/src/commands/selectTypeScriptVersion.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import TypeScriptServiceClientHost from '../typeScriptServiceClientHost'; -import { Command } from '../utils/commandManager'; import { Lazy } from '../utils/lazy'; +import { Command } from './commandManager'; export class SelectTypeScriptVersionCommand implements Command { public readonly id = 'typescript.selectTypeScriptVersion'; diff --git a/extensions/typescript-language-features/src/extension.browser.ts b/extensions/typescript-language-features/src/extension.browser.ts new file mode 100644 index 00000000000..9291e22ae36 --- /dev/null +++ b/extensions/typescript-language-features/src/extension.browser.ts @@ -0,0 +1,81 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { Api, getExtensionApi } from './api'; +import { registerBaseCommands } from './commands/index'; +import { LanguageConfigurationManager } from './languageFeatures/languageConfiguration'; +import { createLazyClientHost, lazilyActivateClient } from './lazyClientHost'; +import { noopRequestCancellerFactory } from './tsServer/cancellation'; +import { noopLogDirectoryProvider } from './tsServer/logDirectoryProvider'; +import { ITypeScriptVersionProvider, TypeScriptVersion, TypeScriptVersionSource } from './tsServer/versionProvider'; +import { WorkerServerProcess } from './tsServer/serverProcess.browser'; +import API from './utils/api'; +import { CommandManager } from './commands/commandManager'; +import { TypeScriptServiceConfiguration } from './utils/configuration'; +import { PluginManager } from './utils/plugins'; + +class StaticVersionProvider implements ITypeScriptVersionProvider { + + constructor( + private readonly _version: TypeScriptVersion + ) { } + + updateConfiguration(_configuration: TypeScriptServiceConfiguration): void { + // noop + } + + get defaultVersion() { return this._version; } + get bundledVersion() { return this._version; } + + readonly globalVersion = undefined; + readonly localVersion = undefined; + readonly localVersions = []; +} + +export function activate( + context: vscode.ExtensionContext +): Api { + const pluginManager = new PluginManager(); + context.subscriptions.push(pluginManager); + + const commandManager = new CommandManager(); + context.subscriptions.push(commandManager); + + context.subscriptions.push(new LanguageConfigurationManager()); + + const onCompletionAccepted = new vscode.EventEmitter(); + context.subscriptions.push(onCompletionAccepted); + + const versionProvider = new StaticVersionProvider( + new TypeScriptVersion( + TypeScriptVersionSource.Bundled, + context.asAbsolutePath('dist/browser/typescript-web/tsserver.web.js'), + API.v400)); + + const lazyClientHost = createLazyClientHost(context, false, { + pluginManager, + commandManager, + logDirectoryProvider: noopLogDirectoryProvider, + cancellerFactory: noopRequestCancellerFactory, + versionProvider, + processFactory: WorkerServerProcess + }, item => { + onCompletionAccepted.fire(item); + }); + + registerBaseCommands(commandManager, lazyClientHost, pluginManager); + + // context.subscriptions.push(task.register(lazyClientHost.map(x => x.serviceClient))); + + import('./languageFeatures/tsconfig').then(module => { + context.subscriptions.push(module.register()); + }); + + context.subscriptions.push(lazilyActivateClient(lazyClientHost, pluginManager)); + + return getExtensionApi(onCompletionAccepted.event, pluginManager); +} + diff --git a/extensions/typescript-language-features/src/extension.ts b/extensions/typescript-language-features/src/extension.ts index 657755e964a..1d6ccd2914d 100644 --- a/extensions/typescript-language-features/src/extension.ts +++ b/extensions/typescript-language-features/src/extension.ts @@ -6,20 +6,17 @@ import * as rimraf from 'rimraf'; import * as vscode from 'vscode'; import { Api, getExtensionApi } from './api'; -import { registerCommands } from './commands/index'; -import { LanguageConfigurationManager } from './features/languageConfiguration'; -import * as task from './features/task'; -import TypeScriptServiceClientHost from './typeScriptServiceClientHost'; -import { flatten } from './utils/arrays'; -import { CommandManager } from './utils/commandManager'; -import * as electron from './utils/electron'; -import * as fileSchemes from './utils/fileSchemes'; -import { standardLanguageDescriptions } from './utils/languageDescription'; -import * as ProjectStatus from './utils/largeProjectStatus'; -import { lazy, Lazy } from './utils/lazy'; -import LogDirectoryProvider from './utils/logDirectoryProvider'; -import ManagedFileContextManager from './utils/managedFileContext'; +import { registerBaseCommands } from './commands/index'; +import { LanguageConfigurationManager } from './languageFeatures/languageConfiguration'; +import { createLazyClientHost, lazilyActivateClient } from './lazyClientHost'; +import { nodeRequestCancellerFactory } from './tsServer/cancellation.electron'; +import { NodeLogDirectoryProvider } from './tsServer/logDirectoryProvider.electron'; +import { ChildServerProcess } from './tsServer/serverProcess.electron'; +import { DiskTypeScriptVersionProvider } from './tsServer/versionProvider.electron'; +import { CommandManager } from './commands/commandManager'; +import { onCaseInsenitiveFileSystem } from './utils/fileSystem.electron'; import { PluginManager } from './utils/plugins'; +import * as temp from './utils/temp.electron'; export function activate( context: vscode.ExtensionContext @@ -33,15 +30,29 @@ export function activate( const onCompletionAccepted = new vscode.EventEmitter(); context.subscriptions.push(onCompletionAccepted); - const lazyClientHost = createLazyClientHost(context, pluginManager, commandManager, item => { + const logDirectoryProvider = new NodeLogDirectoryProvider(context); + const versionProvider = new DiskTypeScriptVersionProvider(); + + context.subscriptions.push(new LanguageConfigurationManager()); + + const lazyClientHost = createLazyClientHost(context, onCaseInsenitiveFileSystem(), { + pluginManager, + commandManager, + logDirectoryProvider, + cancellerFactory: nodeRequestCancellerFactory, + versionProvider, + processFactory: ChildServerProcess, + }, item => { onCompletionAccepted.fire(item); }); - registerCommands(commandManager, lazyClientHost, pluginManager); - context.subscriptions.push(task.register(lazyClientHost.map(x => x.serviceClient))); - context.subscriptions.push(new LanguageConfigurationManager()); + registerBaseCommands(commandManager, lazyClientHost, pluginManager); - import('./features/tsconfig').then(module => { + import('./task/taskProvider').then(module => { + context.subscriptions.push(module.register(lazyClientHost.map(x => x.serviceClient))); + }); + + import('./languageFeatures/tsconfig').then(module => { context.subscriptions.push(module.register()); }); @@ -50,84 +61,6 @@ export function activate( return getExtensionApi(onCompletionAccepted.event, pluginManager); } -function createLazyClientHost( - context: vscode.ExtensionContext, - pluginManager: PluginManager, - commandManager: CommandManager, - onCompletionAccepted: (item: vscode.CompletionItem) => void, -): Lazy { - return lazy(() => { - const logDirectoryProvider = new LogDirectoryProvider(context); - - const clientHost = new TypeScriptServiceClientHost( - standardLanguageDescriptions, - context.workspaceState, - pluginManager, - commandManager, - logDirectoryProvider, - onCompletionAccepted); - - context.subscriptions.push(clientHost); - - clientHost.serviceClient.onReady(() => { - context.subscriptions.push( - ProjectStatus.create( - clientHost.serviceClient, - clientHost.serviceClient.telemetryReporter)); - }); - - return clientHost; - }); -} - -function lazilyActivateClient( - lazyClientHost: Lazy, - pluginManager: PluginManager, -) { - const disposables: vscode.Disposable[] = []; - - const supportedLanguage = flatten([ - ...standardLanguageDescriptions.map(x => x.modeIds), - ...pluginManager.plugins.map(x => x.languages) - ]); - - let hasActivated = false; - const maybeActivate = (textDocument: vscode.TextDocument): boolean => { - if (!hasActivated && isSupportedDocument(supportedLanguage, textDocument)) { - hasActivated = true; - // Force activation - void lazyClientHost.value; - - disposables.push(new ManagedFileContextManager(resource => { - return lazyClientHost.value.serviceClient.toPath(resource); - })); - return true; - } - return false; - }; - - const didActivate = vscode.workspace.textDocuments.some(maybeActivate); - if (!didActivate) { - const openListener = vscode.workspace.onDidOpenTextDocument(doc => { - if (maybeActivate(doc)) { - openListener.dispose(); - } - }, undefined, disposables); - } - - return vscode.Disposable.from(...disposables); -} - -function isSupportedDocument( - supportedLanguage: string[], - document: vscode.TextDocument -): boolean { - if (supportedLanguage.indexOf(document.languageId) < 0) { - return false; - } - return fileSchemes.isSupportedScheme(document.uri.scheme); -} - export function deactivate() { - rimraf.sync(electron.getInstanceDir()); + rimraf.sync(temp.getInstanceTempDir()); } diff --git a/extensions/typescript-language-features/src/features/callHierarchy.ts b/extensions/typescript-language-features/src/languageFeatures/callHierarchy.ts similarity index 93% rename from extensions/typescript-language-features/src/features/callHierarchy.ts rename to extensions/typescript-language-features/src/languageFeatures/callHierarchy.ts index 1bc85e4ca11..a90e68fd27c 100644 --- a/extensions/typescript-language-features/src/features/callHierarchy.ts +++ b/extensions/typescript-language-features/src/languageFeatures/callHierarchy.ts @@ -7,9 +7,10 @@ import * as path from 'path'; import * as vscode from 'vscode'; import type * as Proto from '../protocol'; import * as PConst from '../protocol.const'; -import { ITypeScriptServiceClient } from '../typescriptService'; +import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; -import { conditionalRegistration, requireMinVersion } from '../utils/dependentRegistration'; +import { conditionalRegistration, requireSomeCapability, requireMinVersion } from '../utils/dependentRegistration'; +import { DocumentSelector } from '../utils/documentSelector'; import { parseKindModifier } from '../utils/modifiers'; import * as typeConverters from '../utils/typeConverters'; @@ -117,13 +118,14 @@ function fromProtocolCallHierchyOutgoingCall(item: Proto.CallHierarchyOutgoingCa } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, client: ITypeScriptServiceClient ) { return conditionalRegistration([ requireMinVersion(client, TypeScriptCallHierarchySupport.minVersion), + requireSomeCapability(client, ClientCapability.Semantic), ], () => { - return vscode.languages.registerCallHierarchyProvider(selector, + return vscode.languages.registerCallHierarchyProvider(selector.semantic, new TypeScriptCallHierarchySupport(client)); }); } diff --git a/extensions/typescript-language-features/src/features/baseCodeLensProvider.ts b/extensions/typescript-language-features/src/languageFeatures/codeLens/baseCodeLensProvider.ts similarity index 92% rename from extensions/typescript-language-features/src/features/baseCodeLensProvider.ts rename to extensions/typescript-language-features/src/languageFeatures/codeLens/baseCodeLensProvider.ts index f63853d9ec8..7f0e3e5f097 100644 --- a/extensions/typescript-language-features/src/features/baseCodeLensProvider.ts +++ b/extensions/typescript-language-features/src/languageFeatures/codeLens/baseCodeLensProvider.ts @@ -5,11 +5,11 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import type * as Proto from '../protocol'; -import { ITypeScriptServiceClient } from '../typescriptService'; -import { escapeRegExp } from '../utils/regexp'; -import * as typeConverters from '../utils/typeConverters'; -import { CachedResponse } from '../tsServer/cachedResponse'; +import type * as Proto from '../../protocol'; +import { CachedResponse } from '../../tsServer/cachedResponse'; +import { ITypeScriptServiceClient } from '../../typescriptService'; +import { escapeRegExp } from '../../utils/regexp'; +import * as typeConverters from '../../utils/typeConverters'; const localize = nls.loadMessageBundle(); diff --git a/extensions/typescript-language-features/src/features/implementationsCodeLens.ts b/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts similarity index 84% rename from extensions/typescript-language-features/src/features/implementationsCodeLens.ts rename to extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts index 4e56d6499e7..a340e21bed3 100644 --- a/extensions/typescript-language-features/src/features/implementationsCodeLens.ts +++ b/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts @@ -5,12 +5,13 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import type * as Proto from '../protocol'; -import * as PConst from '../protocol.const'; -import { CachedResponse } from '../tsServer/cachedResponse'; -import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; -import { conditionalRegistration, requireCapability, requireConfiguration } from '../utils/dependentRegistration'; -import * as typeConverters from '../utils/typeConverters'; +import type * as Proto from '../../protocol'; +import * as PConst from '../../protocol.const'; +import { CachedResponse } from '../../tsServer/cachedResponse'; +import { ClientCapability, ITypeScriptServiceClient } from '../../typescriptService'; +import { conditionalRegistration, requireSomeCapability, requireConfiguration } from '../../utils/dependentRegistration'; +import { DocumentSelector } from '../../utils/documentSelector'; +import * as typeConverters from '../../utils/typeConverters'; import { getSymbolRange, ReferencesCodeLens, TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider'; const localize = nls.loadMessageBundle(); @@ -89,16 +90,16 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, modeId: string, client: ITypeScriptServiceClient, cachedResponse: CachedResponse, ) { return conditionalRegistration([ requireConfiguration(modeId, 'implementationsCodeLens.enabled'), - requireCapability(client, ClientCapability.Semantic), + requireSomeCapability(client, ClientCapability.Semantic), ], () => { - return vscode.languages.registerCodeLensProvider(selector, + return vscode.languages.registerCodeLensProvider(selector.semantic, new TypeScriptImplementationsCodeLensProvider(client, cachedResponse)); }); } diff --git a/extensions/typescript-language-features/src/features/referencesCodeLens.ts b/extensions/typescript-language-features/src/languageFeatures/codeLens/referencesCodeLens.ts similarity index 83% rename from extensions/typescript-language-features/src/features/referencesCodeLens.ts rename to extensions/typescript-language-features/src/languageFeatures/codeLens/referencesCodeLens.ts index bfda4f07230..8fe118de5d2 100644 --- a/extensions/typescript-language-features/src/features/referencesCodeLens.ts +++ b/extensions/typescript-language-features/src/languageFeatures/codeLens/referencesCodeLens.ts @@ -5,12 +5,14 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import type * as Proto from '../protocol'; -import * as PConst from '../protocol.const'; -import { CachedResponse } from '../tsServer/cachedResponse'; -import { ITypeScriptServiceClient, ClientCapability } from '../typescriptService'; -import { conditionalRegistration, requireConfiguration, requireCapability } from '../utils/dependentRegistration'; -import * as typeConverters from '../utils/typeConverters'; +import type * as Proto from '../../protocol'; +import * as PConst from '../../protocol.const'; +import { CachedResponse } from '../../tsServer/cachedResponse'; +import { ExectuionTarget } from '../../tsServer/server'; +import { ClientCapability, ITypeScriptServiceClient } from '../../typescriptService'; +import { conditionalRegistration, requireConfiguration, requireSomeCapability } from '../../utils/dependentRegistration'; +import { DocumentSelector } from '../../utils/documentSelector'; +import * as typeConverters from '../../utils/typeConverters'; import { getSymbolRange, ReferencesCodeLens, TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider'; const localize = nls.loadMessageBundle(); @@ -27,7 +29,11 @@ export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLens public async resolveCodeLens(inputCodeLens: vscode.CodeLens, token: vscode.CancellationToken): Promise { const codeLens = inputCodeLens as ReferencesCodeLens; const args = typeConverters.Position.toFileLocationRequestArgs(codeLens.file, codeLens.range.start); - const response = await this.client.execute('references', args, token, { lowPriority: true, cancelOnResourceChange: codeLens.document }); + const response = await this.client.execute('references', args, token, { + lowPriority: true, + executionTarget: ExectuionTarget.Semantic, + cancelOnResourceChange: codeLens.document, + }); if (response.type !== 'response' || !response.body) { codeLens.command = response.type === 'cancelled' ? TypeScriptBaseCodeLensProvider.cancelledCommand @@ -122,16 +128,16 @@ export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLens } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, modeId: string, client: ITypeScriptServiceClient, cachedResponse: CachedResponse, ) { return conditionalRegistration([ requireConfiguration(modeId, 'referencesCodeLens.enabled'), - requireCapability(client, ClientCapability.Semantic), + requireSomeCapability(client, ClientCapability.Semantic), ], () => { - return vscode.languages.registerCodeLensProvider(selector, + return vscode.languages.registerCodeLensProvider(selector.semantic, new TypeScriptReferencesCodeLensProvider(client, cachedResponse, modeId)); }); } diff --git a/extensions/typescript-language-features/src/features/completions.ts b/extensions/typescript-language-features/src/languageFeatures/completions.ts similarity index 89% rename from extensions/typescript-language-features/src/features/completions.ts rename to extensions/typescript-language-features/src/languageFeatures/completions.ts index 5ef98b8b07e..ae827b95b14 100644 --- a/extensions/typescript-language-features/src/features/completions.ts +++ b/extensions/typescript-language-features/src/languageFeatures/completions.ts @@ -5,14 +5,15 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; +import { Command, CommandManager } from '../commands/commandManager'; import type * as Proto from '../protocol'; import * as PConst from '../protocol.const'; -import { ITypeScriptServiceClient, ServerResponse } from '../typescriptService'; +import { ClientCapability, ITypeScriptServiceClient, ServerResponse } from '../typescriptService'; import API from '../utils/api'; import { nulToken } from '../utils/cancellation'; import { applyCodeAction } from '../utils/codeAction'; -import { Command, CommandManager } from '../utils/commandManager'; -import { conditionalRegistration, requireConfiguration } from '../utils/dependentRegistration'; +import { conditionalRegistration, requireConfiguration, requireSomeCapability } from '../utils/dependentRegistration'; +import { DocumentSelector } from '../utils/documentSelector'; import { parseKindModifier } from '../utils/modifiers'; import * as Previewer from '../utils/previewer'; import { snippetForFunctionCall } from '../utils/snippetForFunctionCall'; @@ -329,10 +330,25 @@ class CompletionAcceptedCommand implements Command { public constructor( private readonly onCompletionAccepted: (item: vscode.CompletionItem) => void, + private readonly telemetryReporter: TelemetryReporter, ) { } public execute(item: vscode.CompletionItem) { this.onCompletionAccepted(item); + if (item instanceof MyCompletionItem) { + /* __GDPR__ + "completions.accept" : { + "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, + }); + } } } @@ -421,7 +437,7 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< ) { commandManager.register(new ApplyCompletionCodeActionCommand(this.client)); commandManager.register(new CompositeCommand()); - commandManager.register(new CompletionAcceptedCommand(onCompletionAccepted)); + commandManager.register(new CompletionAcceptedCommand(onCompletionAccepted, this.telemetryReporter)); } public async provideCompletionItems( @@ -470,34 +486,18 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< let dotAccessorContext: DotAccessorContext | undefined; let entries: ReadonlyArray; let metadata: any | undefined; + let response: ServerResponse.Response | undefined; + let duration: number | undefined; if (this.client.apiVersion.gte(API.v300)) { const startTime = Date.now(); - let response: ServerResponse.Response | undefined; try { response = await this.client.interruptGetErr(() => this.client.execute('completionInfo', args, token)); } finally { - const duration: number = Date.now() - startTime; - - /* __GDPR__ - "completions.execute" : { - "duration" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, - "type" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, - "count" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, - "updateGraphDurationMs" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, - "${include}": [ - "${TypeScriptCommonProperties}" - ] - } - */ - this.telemetryReporter.logTelemetry('completions.execute', { - duration: duration, - type: response?.type ?? 'unknown', - count: response?.type === 'response' && response.body ? response.body.entries.length : 0, - updateGraphDurationMs: response?.type === 'response' ? response.performanceData?.updateGraphDurationMs : undefined, - }); + duration = Date.now() - startTime; } if (response.type !== 'response' || !response.body) { + this.logCompletionsTelemetry(duration, response); return null; } isNewIdentifierLocation = response.body.isNewIdentifierLocation; @@ -535,15 +535,49 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< useFuzzyWordRangeLogic: this.client.apiVersion.lt(API.v390), }; + let includesPackageJsonImport = false; const items: MyCompletionItem[] = []; 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; } } + if (duration !== undefined) { + this.logCompletionsTelemetry(duration, response, includesPackageJsonImport); + } return new vscode.CompletionList(items, isIncomplete); } + private logCompletionsTelemetry( + duration: number, + response: ServerResponse.Response | undefined, + includesPackageJsonImport?: boolean + ) { + /* __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" }, + "${include}": [ + "${TypeScriptCommonProperties}" + ] + } + */ + this.telemetryReporter.logTelemetry('completions.execute', { + duration: duration, + type: response?.type ?? 'unknown', + count: response?.type === 'response' && response.body ? response.body.entries.length : 0, + updateGraphDurationMs: response?.type === 'response' ? response.performanceData?.updateGraphDurationMs : undefined, + createAutoImportProviderProgramDurationMs: response?.type === 'response' ? (response.performanceData as Proto.PerformanceData & { createAutoImportProviderProgramDurationMs?: number })?.createAutoImportProviderProgramDurationMs : undefined, + includesPackageJsonImport: includesPackageJsonImport ? 'true' : undefined, + }); + } + private getTsTriggerCharacter(context: vscode.CompletionContext): Proto.CompletionsTriggerCharacter | undefined { switch (context.triggerCharacter) { case '@': // Workaround for https://github.com/Microsoft/TypeScript/issues/27321 @@ -795,7 +829,7 @@ function shouldExcludeCompletionEntry( } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, modeId: string, client: ITypeScriptServiceClient, typingsStatus: TypingsStatus, @@ -806,8 +840,9 @@ export function register( ) { return conditionalRegistration([ requireConfiguration(modeId, 'suggest.enabled'), + requireSomeCapability(client, ClientCapability.EnhancedSyntax, ClientCapability.Semantic), ], () => { - return vscode.languages.registerCompletionItemProvider(selector, + return vscode.languages.registerCompletionItemProvider(selector.syntax, new TypeScriptCompletionItemProvider(client, modeId, typingsStatus, fileConfigurationManager, commandManager, telemetryReporter, onCompletionAccepted), ...TypeScriptCompletionItemProvider.triggerCharacters); }); diff --git a/extensions/typescript-language-features/src/features/definitionProviderBase.ts b/extensions/typescript-language-features/src/languageFeatures/definitionProviderBase.ts similarity index 100% rename from extensions/typescript-language-features/src/features/definitionProviderBase.ts rename to extensions/typescript-language-features/src/languageFeatures/definitionProviderBase.ts diff --git a/extensions/typescript-language-features/src/features/definitions.ts b/extensions/typescript-language-features/src/languageFeatures/definitions.ts similarity index 80% rename from extensions/typescript-language-features/src/features/definitions.ts rename to extensions/typescript-language-features/src/languageFeatures/definitions.ts index fd3a1f10e79..7c01400c206 100644 --- a/extensions/typescript-language-features/src/features/definitions.ts +++ b/extensions/typescript-language-features/src/languageFeatures/definitions.ts @@ -4,8 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { ITypeScriptServiceClient } from '../typescriptService'; +import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; +import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration'; +import { DocumentSelector } from '../utils/documentSelector'; import * as typeConverters from '../utils/typeConverters'; import DefinitionProviderBase from './definitionProviderBase'; @@ -58,9 +60,13 @@ export default class TypeScriptDefinitionProvider extends DefinitionProviderBase } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, client: ITypeScriptServiceClient, ) { - return vscode.languages.registerDefinitionProvider(selector, - new TypeScriptDefinitionProvider(client)); + return conditionalRegistration([ + requireSomeCapability(client, ClientCapability.EnhancedSyntax, ClientCapability.Semantic), + ], () => { + return vscode.languages.registerDefinitionProvider(selector.syntax, + new TypeScriptDefinitionProvider(client)); + }); } diff --git a/extensions/typescript-language-features/src/features/diagnostics.ts b/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts similarity index 95% rename from extensions/typescript-language-features/src/features/diagnostics.ts rename to extensions/typescript-language-features/src/languageFeatures/diagnostics.ts index ec0566d67cb..25a9591cc56 100644 --- a/extensions/typescript-language-features/src/features/diagnostics.ts +++ b/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts @@ -142,17 +142,21 @@ class DiagnosticSettings { } export class DiagnosticsManager extends Disposable { - private readonly _diagnostics = new ResourceMap(); + private readonly _diagnostics: ResourceMap; private readonly _settings = new DiagnosticSettings(); private readonly _currentDiagnostics: vscode.DiagnosticCollection; - private readonly _pendingUpdates = new ResourceMap(); + private readonly _pendingUpdates: ResourceMap; private readonly _updateDelay = 50; constructor( - owner: string + owner: string, + onCaseInsenitiveFileSystem: boolean ) { super(); + this._diagnostics = new ResourceMap(undefined, { onCaseInsenitiveFileSystem }); + this._pendingUpdates = new ResourceMap(undefined, { onCaseInsenitiveFileSystem }); + this._currentDiagnostics = this._register(vscode.languages.createDiagnosticCollection(owner)); } diff --git a/extensions/typescript-language-features/src/features/directiveCommentCompletions.ts b/extensions/typescript-language-features/src/languageFeatures/directiveCommentCompletions.ts similarity index 96% rename from extensions/typescript-language-features/src/features/directiveCommentCompletions.ts rename to extensions/typescript-language-features/src/languageFeatures/directiveCommentCompletions.ts index cfa35571d1d..2b0f683e589 100644 --- a/extensions/typescript-language-features/src/features/directiveCommentCompletions.ts +++ b/extensions/typescript-language-features/src/languageFeatures/directiveCommentCompletions.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; +import { DocumentSelector } from '../utils/documentSelector'; const localize = nls.loadMessageBundle(); @@ -80,10 +81,10 @@ class DirectiveCommentCompletionProvider implements vscode.CompletionItemProvide } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, client: ITypeScriptServiceClient, ) { - return vscode.languages.registerCompletionItemProvider(selector, + return vscode.languages.registerCompletionItemProvider(selector.syntax, new DirectiveCommentCompletionProvider(client), '@'); } diff --git a/extensions/typescript-language-features/src/features/documentHighlight.ts b/extensions/typescript-language-features/src/languageFeatures/documentHighlight.ts similarity index 95% rename from extensions/typescript-language-features/src/features/documentHighlight.ts rename to extensions/typescript-language-features/src/languageFeatures/documentHighlight.ts index 61477e06749..3a7b43ed5eb 100644 --- a/extensions/typescript-language-features/src/features/documentHighlight.ts +++ b/extensions/typescript-language-features/src/languageFeatures/documentHighlight.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import type * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; import { flatten } from '../utils/arrays'; +import { DocumentSelector } from '../utils/documentSelector'; import * as typeConverters from '../utils/typeConverters'; class TypeScriptDocumentHighlightProvider implements vscode.DocumentHighlightProvider { @@ -48,9 +49,9 @@ function convertDocumentHighlight(highlight: Proto.DocumentHighlightsItem): Read } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, client: ITypeScriptServiceClient, ) { - return vscode.languages.registerDocumentHighlightProvider(selector, + return vscode.languages.registerDocumentHighlightProvider(selector.syntax, new TypeScriptDocumentHighlightProvider(client)); } diff --git a/extensions/typescript-language-features/src/features/documentSymbol.ts b/extensions/typescript-language-features/src/languageFeatures/documentSymbol.ts similarity index 97% rename from extensions/typescript-language-features/src/features/documentSymbol.ts rename to extensions/typescript-language-features/src/languageFeatures/documentSymbol.ts index 8b44581c6de..d0b8b9ad901 100644 --- a/extensions/typescript-language-features/src/features/documentSymbol.ts +++ b/extensions/typescript-language-features/src/languageFeatures/documentSymbol.ts @@ -6,10 +6,11 @@ import * as vscode from 'vscode'; import type * as Proto from '../protocol'; import * as PConst from '../protocol.const'; -import { ITypeScriptServiceClient } from '../typescriptService'; -import * as typeConverters from '../utils/typeConverters'; import { CachedResponse } from '../tsServer/cachedResponse'; +import { ITypeScriptServiceClient } from '../typescriptService'; +import { DocumentSelector } from '../utils/documentSelector'; import { parseKindModifier } from '../utils/modifiers'; +import * as typeConverters from '../utils/typeConverters'; const getSymbolKind = (kind: string): vscode.SymbolKind => { switch (kind) { @@ -111,10 +112,10 @@ class TypeScriptDocumentSymbolProvider implements vscode.DocumentSymbolProvider } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, client: ITypeScriptServiceClient, cachedResponse: CachedResponse, ) { - return vscode.languages.registerDocumentSymbolProvider(selector, + return vscode.languages.registerDocumentSymbolProvider(selector.syntax, new TypeScriptDocumentSymbolProvider(client, cachedResponse), { label: 'TypeScript' }); } diff --git a/extensions/typescript-language-features/src/features/fileConfigurationManager.ts b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts similarity index 97% rename from extensions/typescript-language-features/src/features/fileConfigurationManager.ts rename to extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts index 55d5324469b..dbd3513f0d4 100644 --- a/extensions/typescript-language-features/src/features/fileConfigurationManager.ts +++ b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts @@ -30,12 +30,14 @@ function areFileConfigurationsEqual(a: FileConfiguration, b: FileConfiguration): } export default class FileConfigurationManager extends Disposable { - private readonly formatOptions = new ResourceMap>(); + private readonly formatOptions: ResourceMap>; public constructor( - private readonly client: ITypeScriptServiceClient + private readonly client: ITypeScriptServiceClient, + onCaseInsenitiveFileSystem: boolean ) { super(); + this.formatOptions = new ResourceMap(undefined, { onCaseInsenitiveFileSystem }); vscode.workspace.onDidCloseTextDocument(textDocument => { // When a document gets closed delete the cached formatting options. // This is necessary since the tsserver now closed a project when its diff --git a/extensions/typescript-language-features/src/features/fixAll.ts b/extensions/typescript-language-features/src/languageFeatures/fixAll.ts similarity index 94% rename from extensions/typescript-language-features/src/features/fixAll.ts rename to extensions/typescript-language-features/src/languageFeatures/fixAll.ts index 422c26e4131..6a43c535a8b 100644 --- a/extensions/typescript-language-features/src/features/fixAll.ts +++ b/extensions/typescript-language-features/src/languageFeatures/fixAll.ts @@ -6,9 +6,10 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import type * as Proto from '../protocol'; -import { ITypeScriptServiceClient } from '../typescriptService'; +import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; -import { conditionalRegistration, requireMinVersion } from '../utils/dependentRegistration'; +import { conditionalRegistration, requireSomeCapability, requireMinVersion } from '../utils/dependentRegistration'; +import { DocumentSelector } from '../utils/documentSelector'; import * as errorCodes from '../utils/errorCodes'; import * as fixNames from '../utils/fixNames'; import * as typeConverters from '../utils/typeConverters'; @@ -249,15 +250,16 @@ class TypeScriptAutoFixProvider implements vscode.CodeActionProvider { } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, client: ITypeScriptServiceClient, fileConfigurationManager: FileConfigurationManager, diagnosticsManager: DiagnosticsManager, ) { return conditionalRegistration([ - requireMinVersion(client, API.v300) + requireMinVersion(client, API.v300), + requireSomeCapability(client, ClientCapability.Semantic), ], () => { const provider = new TypeScriptAutoFixProvider(client, fileConfigurationManager, diagnosticsManager); - return vscode.languages.registerCodeActionsProvider(selector, provider, provider.metadata); + return vscode.languages.registerCodeActionsProvider(selector.semantic, provider, provider.metadata); }); } diff --git a/extensions/typescript-language-features/src/features/folding.ts b/extensions/typescript-language-features/src/languageFeatures/folding.ts similarity index 94% rename from extensions/typescript-language-features/src/features/folding.ts rename to extensions/typescript-language-features/src/languageFeatures/folding.ts index c61028cb6ef..5b18decb440 100644 --- a/extensions/typescript-language-features/src/features/folding.ts +++ b/extensions/typescript-language-features/src/languageFeatures/folding.ts @@ -9,6 +9,7 @@ import { ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; import { coalesce } from '../utils/arrays'; import { conditionalRegistration, requireMinVersion } from '../utils/dependentRegistration'; +import { DocumentSelector } from '../utils/documentSelector'; import * as typeConverters from '../utils/typeConverters'; class TypeScriptFoldingProvider implements vscode.FoldingRangeProvider { @@ -73,13 +74,13 @@ class TypeScriptFoldingProvider implements vscode.FoldingRangeProvider { } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, client: ITypeScriptServiceClient, ): vscode.Disposable { return conditionalRegistration([ requireMinVersion(client, TypeScriptFoldingProvider.minVersion), ], () => { - return vscode.languages.registerFoldingRangeProvider(selector, + return vscode.languages.registerFoldingRangeProvider(selector.syntax, new TypeScriptFoldingProvider(client)); }); } diff --git a/extensions/typescript-language-features/src/features/formatting.ts b/extensions/typescript-language-features/src/languageFeatures/formatting.ts similarity index 95% rename from extensions/typescript-language-features/src/features/formatting.ts rename to extensions/typescript-language-features/src/languageFeatures/formatting.ts index 525567e0bb2..cc2469774a5 100644 --- a/extensions/typescript-language-features/src/features/formatting.ts +++ b/extensions/typescript-language-features/src/languageFeatures/formatting.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import type * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; import { conditionalRegistration, requireConfiguration } from '../utils/dependentRegistration'; +import { DocumentSelector } from '../utils/documentSelector'; import * as typeConverters from '../utils/typeConverters'; import FileConfigurationManager from './fileConfigurationManager'; @@ -84,7 +85,7 @@ class TypeScriptFormattingProvider implements vscode.DocumentRangeFormattingEdit } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, modeId: string, client: ITypeScriptServiceClient, fileConfigurationManager: FileConfigurationManager @@ -94,8 +95,8 @@ export function register( ], () => { const formattingProvider = new TypeScriptFormattingProvider(client, fileConfigurationManager); return vscode.Disposable.from( - vscode.languages.registerOnTypeFormattingEditProvider(selector, formattingProvider, ';', '}', '\n'), - vscode.languages.registerDocumentRangeFormattingEditProvider(selector, formattingProvider), + vscode.languages.registerOnTypeFormattingEditProvider(selector.syntax, formattingProvider, ';', '}', '\n'), + vscode.languages.registerDocumentRangeFormattingEditProvider(selector.syntax, formattingProvider), ); }); } diff --git a/extensions/typescript-language-features/src/features/hover.ts b/extensions/typescript-language-features/src/languageFeatures/hover.ts similarity index 77% rename from extensions/typescript-language-features/src/features/hover.ts rename to extensions/typescript-language-features/src/languageFeatures/hover.ts index 2ff92d6e308..c6c4860f663 100644 --- a/extensions/typescript-language-features/src/features/hover.ts +++ b/extensions/typescript-language-features/src/languageFeatures/hover.ts @@ -5,7 +5,9 @@ import * as vscode from 'vscode'; import type * as Proto from '../protocol'; -import { ITypeScriptServiceClient } from '../typescriptService'; +import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; +import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration'; +import { DocumentSelector } from '../utils/documentSelector'; import { markdownDocumentation } from '../utils/previewer'; import * as typeConverters from '../utils/typeConverters'; @@ -51,9 +53,13 @@ class TypeScriptHoverProvider implements vscode.HoverProvider { } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, client: ITypeScriptServiceClient ): vscode.Disposable { - return vscode.languages.registerHoverProvider(selector, - new TypeScriptHoverProvider(client)); + return conditionalRegistration([ + requireSomeCapability(client, ClientCapability.EnhancedSyntax, ClientCapability.Semantic), + ], () => { + return vscode.languages.registerHoverProvider(selector.syntax, + new TypeScriptHoverProvider(client)); + }); } diff --git a/extensions/typescript-language-features/src/features/implementations.ts b/extensions/typescript-language-features/src/languageFeatures/implementations.ts similarity index 63% rename from extensions/typescript-language-features/src/features/implementations.ts rename to extensions/typescript-language-features/src/languageFeatures/implementations.ts index c7cdeeb755f..bf3ddfee414 100644 --- a/extensions/typescript-language-features/src/features/implementations.ts +++ b/extensions/typescript-language-features/src/languageFeatures/implementations.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { ITypeScriptServiceClient } from '../typescriptService'; +import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; +import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration'; +import { DocumentSelector } from '../utils/documentSelector'; import DefinitionProviderBase from './definitionProviderBase'; class TypeScriptImplementationProvider extends DefinitionProviderBase implements vscode.ImplementationProvider { @@ -14,9 +16,13 @@ class TypeScriptImplementationProvider extends DefinitionProviderBase implements } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, client: ITypeScriptServiceClient, ) { - return vscode.languages.registerImplementationProvider(selector, - new TypeScriptImplementationProvider(client)); + return conditionalRegistration([ + requireSomeCapability(client, ClientCapability.Semantic), + ], () => { + return vscode.languages.registerImplementationProvider(selector.semantic, + new TypeScriptImplementationProvider(client)); + }); } diff --git a/extensions/typescript-language-features/src/features/jsDocCompletions.ts b/extensions/typescript-language-features/src/languageFeatures/jsDocCompletions.ts similarity index 97% rename from extensions/typescript-language-features/src/features/jsDocCompletions.ts rename to extensions/typescript-language-features/src/languageFeatures/jsDocCompletions.ts index 9c930ac5ad7..6e091af1692 100644 --- a/extensions/typescript-language-features/src/features/jsDocCompletions.ts +++ b/extensions/typescript-language-features/src/languageFeatures/jsDocCompletions.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { ITypeScriptServiceClient } from '../typescriptService'; import { conditionalRegistration, requireConfiguration } from '../utils/dependentRegistration'; +import { DocumentSelector } from '../utils/documentSelector'; import * as typeConverters from '../utils/typeConverters'; @@ -110,14 +111,14 @@ export function templateToSnippet(template: string): vscode.SnippetString { } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, modeId: string, client: ITypeScriptServiceClient, ): vscode.Disposable { return conditionalRegistration([ requireConfiguration(modeId, 'suggest.completeJSDocs') ], () => { - return vscode.languages.registerCompletionItemProvider(selector, + return vscode.languages.registerCompletionItemProvider(selector.syntax, new JsDocCompletionProvider(client), '*'); }); diff --git a/extensions/typescript-language-features/src/features/languageConfiguration.ts b/extensions/typescript-language-features/src/languageFeatures/languageConfiguration.ts similarity index 100% rename from extensions/typescript-language-features/src/features/languageConfiguration.ts rename to extensions/typescript-language-features/src/languageFeatures/languageConfiguration.ts diff --git a/extensions/typescript-language-features/src/features/organizeImports.ts b/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts similarity index 87% rename from extensions/typescript-language-features/src/features/organizeImports.ts rename to extensions/typescript-language-features/src/languageFeatures/organizeImports.ts index 186d12e3f2e..218cda4103f 100644 --- a/extensions/typescript-language-features/src/features/organizeImports.ts +++ b/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts @@ -6,11 +6,12 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import type * as Proto from '../protocol'; -import { ITypeScriptServiceClient } from '../typescriptService'; +import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; import { nulToken } from '../utils/cancellation'; -import { Command, CommandManager } from '../utils/commandManager'; -import { conditionalRegistration, requireMinVersion } from '../utils/dependentRegistration'; +import { Command, CommandManager } from '../commands/commandManager'; +import { conditionalRegistration, requireMinVersion, requireSomeCapability } from '../utils/dependentRegistration'; +import { DocumentSelector } from '../utils/documentSelector'; import { TelemetryReporter } from '../utils/telemetry'; import * as typeconverts from '../utils/typeConverters'; import FileConfigurationManager from './fileConfigurationManager'; @@ -99,17 +100,18 @@ export class OrganizeImportsCodeActionProvider implements vscode.CodeActionProvi } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, client: ITypeScriptServiceClient, commandManager: CommandManager, fileConfigurationManager: FileConfigurationManager, telemetryReporter: TelemetryReporter, ) { return conditionalRegistration([ - requireMinVersion(client, OrganizeImportsCodeActionProvider.minVersion) + requireMinVersion(client, OrganizeImportsCodeActionProvider.minVersion), + requireSomeCapability(client, ClientCapability.Semantic), ], () => { const organizeImportsProvider = new OrganizeImportsCodeActionProvider(client, commandManager, fileConfigurationManager, telemetryReporter); - return vscode.languages.registerCodeActionsProvider(selector, + return vscode.languages.registerCodeActionsProvider(selector.semantic, organizeImportsProvider, organizeImportsProvider.metadata); }); diff --git a/extensions/typescript-language-features/src/features/quickFix.ts b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts similarity index 96% rename from extensions/typescript-language-features/src/features/quickFix.ts rename to extensions/typescript-language-features/src/languageFeatures/quickFix.ts index 1d6fd6aa349..0965a574721 100644 --- a/extensions/typescript-language-features/src/features/quickFix.ts +++ b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts @@ -5,20 +5,21 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; +import { Command, CommandManager } from '../commands/commandManager'; import type * as Proto from '../protocol'; -import { ITypeScriptServiceClient, ClientCapability } from '../typescriptService'; +import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; import { nulToken } from '../utils/cancellation'; import { applyCodeActionCommands, getEditForCodeAction } from '../utils/codeAction'; -import { Command, CommandManager } from '../utils/commandManager'; +import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration'; +import { DocumentSelector } from '../utils/documentSelector'; import * as fixNames from '../utils/fixNames'; import { memoize } from '../utils/memoize'; +import { equals } from '../utils/objects'; import { TelemetryReporter } from '../utils/telemetry'; import * as typeConverters from '../utils/typeConverters'; import { DiagnosticsManager } from './diagnostics'; import FileConfigurationManager from './fileConfigurationManager'; -import { equals } from '../utils/objects'; -import { conditionalRegistration, requireCapability } from '../utils/dependentRegistration'; const localize = nls.loadMessageBundle(); @@ -403,7 +404,7 @@ function isPreferredFix( } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, client: ITypeScriptServiceClient, fileConfigurationManager: FileConfigurationManager, commandManager: CommandManager, @@ -411,9 +412,9 @@ export function register( telemetryReporter: TelemetryReporter ) { return conditionalRegistration([ - requireCapability(client, ClientCapability.Semantic), + requireSomeCapability(client, ClientCapability.Semantic), ], () => { - return vscode.languages.registerCodeActionsProvider(selector, + return vscode.languages.registerCodeActionsProvider(selector.semantic, new TypeScriptQuickFixProvider(client, fileConfigurationManager, commandManager, diagnosticsManager, telemetryReporter), TypeScriptQuickFixProvider.metadata); }); diff --git a/extensions/typescript-language-features/src/features/refactor.ts b/extensions/typescript-language-features/src/languageFeatures/refactor.ts similarity index 97% rename from extensions/typescript-language-features/src/features/refactor.ts rename to extensions/typescript-language-features/src/languageFeatures/refactor.ts index a56267acb42..fe1d3eeee3c 100644 --- a/extensions/typescript-language-features/src/features/refactor.ts +++ b/extensions/typescript-language-features/src/languageFeatures/refactor.ts @@ -5,13 +5,14 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; +import { Command, CommandManager } from '../commands/commandManager'; import { LearnMoreAboutRefactoringsCommand } from '../commands/learnMoreAboutRefactorings'; import type * as Proto from '../protocol'; import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; import { nulToken } from '../utils/cancellation'; -import { Command, CommandManager } from '../utils/commandManager'; -import { conditionalRegistration, requireCapability, requireMinVersion } from '../utils/dependentRegistration'; +import { conditionalRegistration, requireMinVersion, requireSomeCapability } from '../utils/dependentRegistration'; +import { DocumentSelector } from '../utils/documentSelector'; import * as fileSchemes from '../utils/fileSchemes'; import { TelemetryReporter } from '../utils/telemetry'; import * as typeConverters from '../utils/typeConverters'; @@ -396,7 +397,7 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider { } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, client: ITypeScriptServiceClient, formattingOptionsManager: FormattingOptionsManager, commandManager: CommandManager, @@ -404,9 +405,9 @@ export function register( ) { return conditionalRegistration([ requireMinVersion(client, TypeScriptRefactorProvider.minVersion), - requireCapability(client, ClientCapability.Semantic), + requireSomeCapability(client, ClientCapability.Semantic), ], () => { - return vscode.languages.registerCodeActionsProvider(selector, + return vscode.languages.registerCodeActionsProvider(selector.semantic, new TypeScriptRefactorProvider(client, formattingOptionsManager, commandManager, telemetryReporter), TypeScriptRefactorProvider.metadata); }); diff --git a/extensions/typescript-language-features/src/features/references.ts b/extensions/typescript-language-features/src/languageFeatures/references.ts similarity index 74% rename from extensions/typescript-language-features/src/features/references.ts rename to extensions/typescript-language-features/src/languageFeatures/references.ts index d77ecc5b10b..1ee8d150429 100644 --- a/extensions/typescript-language-features/src/features/references.ts +++ b/extensions/typescript-language-features/src/languageFeatures/references.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { ITypeScriptServiceClient } from '../typescriptService'; +import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; +import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration'; +import { DocumentSelector } from '../utils/documentSelector'; import * as typeConverters from '../utils/typeConverters'; class TypeScriptReferenceSupport implements vscode.ReferenceProvider { @@ -42,9 +44,13 @@ class TypeScriptReferenceSupport implements vscode.ReferenceProvider { } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, client: ITypeScriptServiceClient ) { - return vscode.languages.registerReferenceProvider(selector, - new TypeScriptReferenceSupport(client)); + return conditionalRegistration([ + requireSomeCapability(client, ClientCapability.EnhancedSyntax, ClientCapability.Semantic), + ], () => { + return vscode.languages.registerReferenceProvider(selector.syntax, + new TypeScriptReferenceSupport(client)); + }); } diff --git a/extensions/typescript-language-features/src/features/rename.ts b/extensions/typescript-language-features/src/languageFeatures/rename.ts similarity index 93% rename from extensions/typescript-language-features/src/features/rename.ts rename to extensions/typescript-language-features/src/languageFeatures/rename.ts index da784760d66..e3e1b3c694e 100644 --- a/extensions/typescript-language-features/src/features/rename.ts +++ b/extensions/typescript-language-features/src/languageFeatures/rename.ts @@ -9,7 +9,8 @@ import * as nls from 'vscode-nls'; import type * as Proto from '../protocol'; import { ClientCapability, ITypeScriptServiceClient, ServerResponse } from '../typescriptService'; import API from '../utils/api'; -import { conditionalRegistration, requireCapability } from '../utils/dependentRegistration'; +import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration'; +import { DocumentSelector } from '../utils/documentSelector'; import * as typeConverters from '../utils/typeConverters'; import FileConfigurationManager from './fileConfigurationManager'; @@ -138,14 +139,14 @@ class TypeScriptRenameProvider implements vscode.RenameProvider { } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, client: ITypeScriptServiceClient, fileConfigurationManager: FileConfigurationManager, ) { return conditionalRegistration([ - requireCapability(client, ClientCapability.Semantic), + requireSomeCapability(client, ClientCapability.Semantic), ], () => { - return vscode.languages.registerRenameProvider(selector, + return vscode.languages.registerRenameProvider(selector.semantic, new TypeScriptRenameProvider(client, fileConfigurationManager)); }); } diff --git a/extensions/typescript-language-features/src/features/semanticTokens.ts b/extensions/typescript-language-features/src/languageFeatures/semanticTokens.ts similarity index 96% rename from extensions/typescript-language-features/src/features/semanticTokens.ts rename to extensions/typescript-language-features/src/languageFeatures/semanticTokens.ts index 6174b51c8e5..c404ac0b95a 100644 --- a/extensions/typescript-language-features/src/features/semanticTokens.ts +++ b/extensions/typescript-language-features/src/languageFeatures/semanticTokens.ts @@ -9,7 +9,8 @@ import * as vscode from 'vscode'; import * as Proto from '../protocol'; import { ClientCapability, ExecConfig, ITypeScriptServiceClient, ServerResponse } from '../typescriptService'; import API from '../utils/api'; -import { conditionalRegistration, requireCapability, requireMinVersion } from '../utils/dependentRegistration'; +import { conditionalRegistration, requireSomeCapability, requireMinVersion } from '../utils/dependentRegistration'; +import { DocumentSelector } from '../utils/documentSelector'; const minTypeScriptVersion = API.fromVersionString(`${VersionRequirement.major}.${VersionRequirement.minor}`); @@ -17,15 +18,18 @@ const minTypeScriptVersion = API.fromVersionString(`${VersionRequirement.major}. // as we don't do deltas, for performance reasons, don't compute semantic tokens for documents above that limit const CONTENT_LENGTH_LIMIT = 100000; -export function register(selector: vscode.DocumentSelector, client: ITypeScriptServiceClient) { +export function register( + selector: DocumentSelector, + client: ITypeScriptServiceClient, +) { return conditionalRegistration([ requireMinVersion(client, minTypeScriptVersion), - requireCapability(client, ClientCapability.Semantic), + requireSomeCapability(client, ClientCapability.Semantic), ], () => { const provider = new DocumentSemanticTokensProvider(client); return vscode.Disposable.from( // register only as a range provider - vscode.languages.registerDocumentRangeSemanticTokensProvider(selector, provider, provider.getLegend()), + vscode.languages.registerDocumentRangeSemanticTokensProvider(selector.semantic, provider, provider.getLegend()), ); }); } diff --git a/extensions/typescript-language-features/src/features/signatureHelp.ts b/extensions/typescript-language-features/src/languageFeatures/signatureHelp.ts similarity index 86% rename from extensions/typescript-language-features/src/features/signatureHelp.ts rename to extensions/typescript-language-features/src/languageFeatures/signatureHelp.ts index 8e751f8c114..93901b9bb25 100644 --- a/extensions/typescript-language-features/src/features/signatureHelp.ts +++ b/extensions/typescript-language-features/src/languageFeatures/signatureHelp.ts @@ -5,7 +5,9 @@ import * as vscode from 'vscode'; import type * as Proto from '../protocol'; -import { ITypeScriptServiceClient } from '../typescriptService'; +import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; +import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration'; +import { DocumentSelector } from '../utils/documentSelector'; import * as Previewer from '../utils/previewer'; import * as typeConverters from '../utils/typeConverters'; @@ -120,12 +122,16 @@ function toTsTriggerReason(context: vscode.SignatureHelpContext): Proto.Signatur } } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, client: ITypeScriptServiceClient, ) { - return vscode.languages.registerSignatureHelpProvider(selector, - new TypeScriptSignatureHelpProvider(client), { - triggerCharacters: TypeScriptSignatureHelpProvider.triggerCharacters, - retriggerCharacters: TypeScriptSignatureHelpProvider.retriggerCharacters + return conditionalRegistration([ + requireSomeCapability(client, ClientCapability.EnhancedSyntax, ClientCapability.Semantic), + ], () => { + return vscode.languages.registerSignatureHelpProvider(selector.syntax, + new TypeScriptSignatureHelpProvider(client), { + triggerCharacters: TypeScriptSignatureHelpProvider.triggerCharacters, + retriggerCharacters: TypeScriptSignatureHelpProvider.retriggerCharacters + }); }); } diff --git a/extensions/typescript-language-features/src/features/smartSelect.ts b/extensions/typescript-language-features/src/languageFeatures/smartSelect.ts similarity index 94% rename from extensions/typescript-language-features/src/features/smartSelect.ts rename to extensions/typescript-language-features/src/languageFeatures/smartSelect.ts index e61c168bd8c..f769347f15c 100644 --- a/extensions/typescript-language-features/src/features/smartSelect.ts +++ b/extensions/typescript-language-features/src/languageFeatures/smartSelect.ts @@ -8,6 +8,7 @@ import type * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; import { conditionalRegistration, requireMinVersion } from '../utils/dependentRegistration'; +import { DocumentSelector } from '../utils/documentSelector'; import * as typeConverters from '../utils/typeConverters'; class SmartSelection implements vscode.SelectionRangeProvider { @@ -49,12 +50,12 @@ class SmartSelection implements vscode.SelectionRangeProvider { } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, client: ITypeScriptServiceClient, ) { return conditionalRegistration([ requireMinVersion(client, SmartSelection.minVersion), ], () => { - return vscode.languages.registerSelectionRangeProvider(selector, new SmartSelection(client)); + return vscode.languages.registerSelectionRangeProvider(selector.syntax, new SmartSelection(client)); }); } diff --git a/extensions/typescript-language-features/src/features/tagClosing.ts b/extensions/typescript-language-features/src/languageFeatures/tagClosing.ts similarity index 97% rename from extensions/typescript-language-features/src/features/tagClosing.ts rename to extensions/typescript-language-features/src/languageFeatures/tagClosing.ts index 40fee040f3e..289ce73b293 100644 --- a/extensions/typescript-language-features/src/features/tagClosing.ts +++ b/extensions/typescript-language-features/src/languageFeatures/tagClosing.ts @@ -9,6 +9,7 @@ import { ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; import { conditionalRegistration, requireMinVersion, requireConfiguration, Condition } from '../utils/dependentRegistration'; import { Disposable } from '../utils/dispose'; +import { DocumentSelector } from '../utils/documentSelector'; import * as typeConverters from '../utils/typeConverters'; class TagClosing extends Disposable { @@ -151,13 +152,13 @@ function requireActiveDocument( } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, modeId: string, client: ITypeScriptServiceClient, ) { return conditionalRegistration([ requireMinVersion(client, TagClosing.minVersion), requireConfiguration(modeId, 'autoClosingTags'), - requireActiveDocument(selector) + requireActiveDocument(selector.syntax) ], () => new TagClosing(client)); } diff --git a/extensions/typescript-language-features/src/features/tsconfig.ts b/extensions/typescript-language-features/src/languageFeatures/tsconfig.ts similarity index 100% rename from extensions/typescript-language-features/src/features/tsconfig.ts rename to extensions/typescript-language-features/src/languageFeatures/tsconfig.ts diff --git a/extensions/typescript-language-features/src/features/typeDefinitions.ts b/extensions/typescript-language-features/src/languageFeatures/typeDefinitions.ts similarity index 62% rename from extensions/typescript-language-features/src/features/typeDefinitions.ts rename to extensions/typescript-language-features/src/languageFeatures/typeDefinitions.ts index 6f63e44df0a..4aef4f41ed4 100644 --- a/extensions/typescript-language-features/src/features/typeDefinitions.ts +++ b/extensions/typescript-language-features/src/languageFeatures/typeDefinitions.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { ITypeScriptServiceClient } from '../typescriptService'; +import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; +import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration'; +import { DocumentSelector } from '../utils/documentSelector'; import DefinitionProviderBase from './definitionProviderBase'; export default class TypeScriptTypeDefinitionProvider extends DefinitionProviderBase implements vscode.TypeDefinitionProvider { @@ -14,9 +16,13 @@ export default class TypeScriptTypeDefinitionProvider extends DefinitionProvider } export function register( - selector: vscode.DocumentSelector, + selector: DocumentSelector, client: ITypeScriptServiceClient, ) { - return vscode.languages.registerTypeDefinitionProvider(selector, - new TypeScriptTypeDefinitionProvider(client)); + return conditionalRegistration([ + requireSomeCapability(client, ClientCapability.EnhancedSyntax, ClientCapability.Semantic), + ], () => { + return vscode.languages.registerTypeDefinitionProvider(selector.syntax, + new TypeScriptTypeDefinitionProvider(client)); + }); } diff --git a/extensions/typescript-language-features/src/features/updatePathsOnRename.ts b/extensions/typescript-language-features/src/languageFeatures/updatePathsOnRename.ts similarity index 98% rename from extensions/typescript-language-features/src/features/updatePathsOnRename.ts rename to extensions/typescript-language-features/src/languageFeatures/updatePathsOnRename.ts index 84171e18fb7..2c6354c01a8 100644 --- a/extensions/typescript-language-features/src/features/updatePathsOnRename.ts +++ b/extensions/typescript-language-features/src/languageFeatures/updatePathsOnRename.ts @@ -11,7 +11,7 @@ import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService import API from '../utils/api'; import { Delayer } from '../utils/async'; import { nulToken } from '../utils/cancellation'; -import { conditionalRegistration, requireCapability, requireMinVersion } from '../utils/dependentRegistration'; +import { conditionalRegistration, requireSomeCapability, requireMinVersion } from '../utils/dependentRegistration'; import { Disposable } from '../utils/dispose'; import * as fileSchemes from '../utils/fileSchemes'; import { doesResourceLookLikeATypeScriptFile } from '../utils/languageDescription'; @@ -296,7 +296,7 @@ export function register( ) { return conditionalRegistration([ requireMinVersion(client, UpdateImportsOnFileRenameHandler.minVersion), - requireCapability(client, ClientCapability.Semantic), + requireSomeCapability(client, ClientCapability.Semantic), ], () => { return new UpdateImportsOnFileRenameHandler(client, fileConfigurationManager, handles); }); diff --git a/extensions/typescript-language-features/src/features/workspaceSymbols.ts b/extensions/typescript-language-features/src/languageFeatures/workspaceSymbols.ts similarity index 100% rename from extensions/typescript-language-features/src/features/workspaceSymbols.ts rename to extensions/typescript-language-features/src/languageFeatures/workspaceSymbols.ts diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index ee040667b68..92ab84be308 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -5,15 +5,15 @@ import { basename } from 'path'; import * as vscode from 'vscode'; +import { DiagnosticKind } from './languageFeatures/diagnostics'; +import FileConfigurationManager from './languageFeatures/fileConfigurationManager'; import { CachedResponse } from './tsServer/cachedResponse'; -import { DiagnosticKind } from './features/diagnostics'; -import FileConfigurationManager from './features/fileConfigurationManager'; import TypeScriptServiceClient from './typescriptServiceClient'; -import { CommandManager } from './utils/commandManager'; +import { CommandManager } from './commands/commandManager'; import { Disposable } from './utils/dispose'; +import { DocumentSelector } from './utils/documentSelector'; import * as fileSchemes from './utils/fileSchemes'; import { LanguageDescription } from './utils/languageDescription'; -import { memoize } from './utils/memoize'; import { TelemetryReporter } from './utils/telemetry'; import TypingsStatus from './utils/typingsStatus'; @@ -39,15 +39,17 @@ export default class LanguageProvider extends Disposable { client.onReady(() => this.registerProviders()); } - @memoize - private get documentSelector(): vscode.DocumentFilter[] { - const documentSelector = []; + private get documentSelector(): DocumentSelector { + const semantic: vscode.DocumentFilter[] = []; + const syntax: vscode.DocumentFilter[] = []; for (const language of this.description.modeIds) { - for (const scheme of fileSchemes.supportedSchemes) { - documentSelector.push({ language, scheme }); + syntax.push({ language }); + for (const scheme of fileSchemes.semanticSupportedSchemes) { + semantic.push({ language, scheme }); } } - return documentSelector; + + return { semantic, syntax }; } private async registerProviders(): Promise { @@ -56,30 +58,30 @@ export default class LanguageProvider extends Disposable { const cachedResponse = new CachedResponse(); await Promise.all([ - import('./features/completions').then(provider => this._register(provider.register(selector, this.description.id, this.client, this.typingsStatus, this.fileConfigurationManager, this.commandManager, this.telemetryReporter, this.onCompletionAccepted))), - import('./features/definitions').then(provider => this._register(provider.register(selector, this.client))), - import('./features/directiveCommentCompletions').then(provider => this._register(provider.register(selector, this.client))), - import('./features/documentHighlight').then(provider => this._register(provider.register(selector, this.client))), - import('./features/documentSymbol').then(provider => this._register(provider.register(selector, this.client, cachedResponse))), - import('./features/folding').then(provider => this._register(provider.register(selector, this.client))), - import('./features/formatting').then(provider => this._register(provider.register(selector, this.description.id, this.client, this.fileConfigurationManager))), - import('./features/hover').then(provider => this._register(provider.register(selector, this.client))), - import('./features/implementations').then(provider => this._register(provider.register(selector, this.client))), - import('./features/implementationsCodeLens').then(provider => this._register(provider.register(selector, this.description.id, this.client, cachedResponse))), - import('./features/jsDocCompletions').then(provider => this._register(provider.register(selector, this.description.id, this.client))), - import('./features/organizeImports').then(provider => this._register(provider.register(selector, this.client, this.commandManager, this.fileConfigurationManager, this.telemetryReporter))), - import('./features/quickFix').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.client.diagnosticsManager, this.telemetryReporter))), - import('./features/fixAll').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager, this.client.diagnosticsManager))), - import('./features/refactor').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.telemetryReporter))), - import('./features/references').then(provider => this._register(provider.register(selector, this.client))), - import('./features/referencesCodeLens').then(provider => this._register(provider.register(selector, this.description.id, this.client, cachedResponse))), - import('./features/rename').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager))), - import('./features/smartSelect').then(provider => this._register(provider.register(selector, this.client))), - import('./features/signatureHelp').then(provider => this._register(provider.register(selector, this.client))), - import('./features/tagClosing').then(provider => this._register(provider.register(selector, this.description.id, this.client))), - import('./features/typeDefinitions').then(provider => this._register(provider.register(selector, this.client))), - import('./features/semanticTokens').then(provider => this._register(provider.register(selector, this.client))), - import('./features/callHierarchy').then(provider => this._register(provider.register(selector, this.client))), + import('./languageFeatures/completions').then(provider => this._register(provider.register(selector, this.description.id, this.client, this.typingsStatus, this.fileConfigurationManager, this.commandManager, this.telemetryReporter, this.onCompletionAccepted))), + import('./languageFeatures/definitions').then(provider => this._register(provider.register(selector, this.client))), + import('./languageFeatures/directiveCommentCompletions').then(provider => this._register(provider.register(selector, this.client))), + import('./languageFeatures/documentHighlight').then(provider => this._register(provider.register(selector, this.client))), + import('./languageFeatures/documentSymbol').then(provider => this._register(provider.register(selector, this.client, cachedResponse))), + import('./languageFeatures/folding').then(provider => this._register(provider.register(selector, this.client))), + import('./languageFeatures/formatting').then(provider => this._register(provider.register(selector, this.description.id, this.client, this.fileConfigurationManager))), + import('./languageFeatures/hover').then(provider => this._register(provider.register(selector, this.client))), + import('./languageFeatures/implementations').then(provider => this._register(provider.register(selector, this.client))), + import('./languageFeatures/codeLens/implementationsCodeLens').then(provider => this._register(provider.register(selector, this.description.id, this.client, cachedResponse))), + import('./languageFeatures/jsDocCompletions').then(provider => this._register(provider.register(selector, this.description.id, this.client))), + import('./languageFeatures/organizeImports').then(provider => this._register(provider.register(selector, this.client, this.commandManager, this.fileConfigurationManager, this.telemetryReporter))), + import('./languageFeatures/quickFix').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.client.diagnosticsManager, this.telemetryReporter))), + import('./languageFeatures/fixAll').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager, this.client.diagnosticsManager))), + import('./languageFeatures/refactor').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.telemetryReporter))), + import('./languageFeatures/references').then(provider => this._register(provider.register(selector, this.client))), + import('./languageFeatures/codeLens/referencesCodeLens').then(provider => this._register(provider.register(selector, this.description.id, this.client, cachedResponse))), + import('./languageFeatures/rename').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager))), + import('./languageFeatures/smartSelect').then(provider => this._register(provider.register(selector, this.client))), + import('./languageFeatures/signatureHelp').then(provider => this._register(provider.register(selector, this.client))), + import('./languageFeatures/tagClosing').then(provider => this._register(provider.register(selector, this.description.id, this.client))), + import('./languageFeatures/typeDefinitions').then(provider => this._register(provider.register(selector, this.client))), + import('./languageFeatures/semanticTokens').then(provider => this._register(provider.register(selector, this.client))), + import('./languageFeatures/callHierarchy').then(provider => this._register(provider.register(selector, this.client))), ]); } diff --git a/extensions/typescript-language-features/src/lazyClientHost.ts b/extensions/typescript-language-features/src/lazyClientHost.ts new file mode 100644 index 00000000000..7f136285215 --- /dev/null +++ b/extensions/typescript-language-features/src/lazyClientHost.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { CommandManager } from './commands/commandManager'; +import { OngoingRequestCancellerFactory } from './tsServer/cancellation'; +import { ILogDirectoryProvider } from './tsServer/logDirectoryProvider'; +import { TsServerProcessFactory } from './tsServer/server'; +import { ITypeScriptVersionProvider } from './tsServer/versionProvider'; +import TypeScriptServiceClientHost from './typeScriptServiceClientHost'; +import { flatten } from './utils/arrays'; +import { standardLanguageDescriptions } from './utils/languageDescription'; +import { lazy, Lazy } from './utils/lazy'; +import ManagedFileContextManager from './utils/managedFileContext'; +import { PluginManager } from './utils/plugins'; + +export function createLazyClientHost( + context: vscode.ExtensionContext, + onCaseInsenitiveFileSystem: boolean, + services: { + pluginManager: PluginManager, + commandManager: CommandManager, + logDirectoryProvider: ILogDirectoryProvider, + cancellerFactory: OngoingRequestCancellerFactory, + versionProvider: ITypeScriptVersionProvider, + processFactory: TsServerProcessFactory, + }, + onCompletionAccepted: (item: vscode.CompletionItem) => void, +): Lazy { + return lazy(() => { + const clientHost = new TypeScriptServiceClientHost( + standardLanguageDescriptions, + context.workspaceState, + onCaseInsenitiveFileSystem, + services, + onCompletionAccepted); + + context.subscriptions.push(clientHost); + + return clientHost; + }); +} + +export function lazilyActivateClient( + lazyClientHost: Lazy, + pluginManager: PluginManager, +): vscode.Disposable { + const disposables: vscode.Disposable[] = []; + + const supportedLanguage = flatten([ + ...standardLanguageDescriptions.map(x => x.modeIds), + ...pluginManager.plugins.map(x => x.languages) + ]); + + let hasActivated = false; + const maybeActivate = (textDocument: vscode.TextDocument): boolean => { + if (!hasActivated && isSupportedDocument(supportedLanguage, textDocument)) { + hasActivated = true; + // Force activation + void lazyClientHost.value; + + disposables.push(new ManagedFileContextManager(resource => { + return lazyClientHost.value.serviceClient.toPath(resource); + })); + return true; + } + return false; + }; + + const didActivate = vscode.workspace.textDocuments.some(maybeActivate); + if (!didActivate) { + const openListener = vscode.workspace.onDidOpenTextDocument(doc => { + if (maybeActivate(doc)) { + openListener.dispose(); + } + }, undefined, disposables); + } + + return vscode.Disposable.from(...disposables); +} + +function isSupportedDocument( + supportedLanguage: readonly string[], + document: vscode.TextDocument +): boolean { + return supportedLanguage.indexOf(document.languageId) >= 0; +} diff --git a/extensions/typescript-language-features/src/features/task.ts b/extensions/typescript-language-features/src/task/taskProvider.ts similarity index 99% rename from extensions/typescript-language-features/src/features/task.ts rename to extensions/typescript-language-features/src/task/taskProvider.ts index 4fa8bba2bdb..0024a3596f3 100644 --- a/extensions/typescript-language-features/src/features/task.ts +++ b/extensions/typescript-language-features/src/task/taskProvider.ts @@ -11,7 +11,7 @@ import { ITypeScriptServiceClient, ServerResponse } from '../typescriptService'; import { isTsConfigFileName } from '../utils/languageDescription'; import { Lazy } from '../utils/lazy'; import { isImplicitProjectConfigFile } from '../utils/tsconfig'; -import TsConfigProvider, { TSConfig } from '../utils/tsconfigProvider'; +import { TSConfig, TsConfigProvider } from './tsconfigProvider'; const localize = nls.loadMessageBundle(); diff --git a/extensions/typescript-language-features/src/utils/tsconfigProvider.ts b/extensions/typescript-language-features/src/task/tsconfigProvider.ts similarity index 96% rename from extensions/typescript-language-features/src/utils/tsconfigProvider.ts rename to extensions/typescript-language-features/src/task/tsconfigProvider.ts index e77dfb306a9..d44b828e384 100644 --- a/extensions/typescript-language-features/src/utils/tsconfigProvider.ts +++ b/extensions/typescript-language-features/src/task/tsconfigProvider.ts @@ -12,7 +12,7 @@ export interface TSConfig { readonly workspaceFolder?: vscode.WorkspaceFolder; } -export default class TsConfigProvider { +export class TsConfigProvider { public async getConfigsForWorkspace(): Promise> { if (!vscode.workspace.workspaceFolders) { return []; diff --git a/extensions/typescript-language-features/src/test/jsdocSnippet.test.ts b/extensions/typescript-language-features/src/test/jsdocSnippet.test.ts index a982e46221f..d9e17a75644 100644 --- a/extensions/typescript-language-features/src/test/jsdocSnippet.test.ts +++ b/extensions/typescript-language-features/src/test/jsdocSnippet.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import 'mocha'; -import { templateToSnippet } from '../features/jsDocCompletions'; +import { templateToSnippet } from '../languageFeatures/jsDocCompletions'; const joinLines = (...args: string[]) => args.join('\n'); diff --git a/extensions/typescript-language-features/src/test/server.test.ts b/extensions/typescript-language-features/src/test/server.test.ts index d7189bdfb49..7e27e366b5c 100644 --- a/extensions/typescript-language-features/src/test/server.test.ts +++ b/extensions/typescript-language-features/src/test/server.test.ts @@ -6,12 +6,13 @@ import * as assert from 'assert'; import 'mocha'; import * as stream from 'stream'; -import { PipeRequestCanceller, TsServerProcess, ProcessBasedTsServer } from '../tsServer/server'; +import type * as Proto from '../protocol'; +import { NodeRequestCanceller } from '../tsServer/cancellation.electron'; +import { ProcessBasedTsServer, TsServerProcess } from '../tsServer/server'; import { nulToken } from '../utils/cancellation'; -import Logger from '../utils/logger'; +import { Logger } from '../utils/logger'; import { TelemetryReporter } from '../utils/telemetry'; import Tracer from '../utils/tracer'; -import type * as Proto from '../protocol'; const NoopTelemetryReporter = new class implements TelemetryReporter { @@ -63,7 +64,7 @@ suite('Server', () => { test('should send requests with increasing sequence numbers', async () => { const process = new FakeServerProcess(); - const server = new ProcessBasedTsServer('semantic', process, undefined, new PipeRequestCanceller('semantic', undefined, tracer), undefined!, NoopTelemetryReporter, tracer); + const server = new ProcessBasedTsServer('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/features/bufferSyncSupport.ts b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts similarity index 96% rename from extensions/typescript-language-features/src/features/bufferSyncSupport.ts rename to extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts index 7fb00fbc894..fdc552a293c 100644 --- a/extensions/typescript-language-features/src/features/bufferSyncSupport.ts +++ b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts @@ -68,11 +68,16 @@ type BufferOperation = CloseOperation | OpenOperation | ChangeOperation; */ class BufferSynchronizer { - private readonly _pending = new ResourceMap(); + private readonly _pending: ResourceMap; constructor( - private readonly client: ITypeScriptServiceClient - ) { } + private readonly client: ITypeScriptServiceClient, + onCaseInsenitiveFileSystem: boolean + ) { + this._pending = new ResourceMap(undefined, { + onCaseInsenitiveFileSystem + }); + } public open(resource: vscode.Uri, args: Proto.OpenRequestArgs) { if (this.supportsBatching) { @@ -275,7 +280,7 @@ class PendingDiagnostics extends ResourceMap { .sort((a, b) => a.value - b.value) .map(entry => entry.resource); - const map = new ResourceMap(); + const map = new ResourceMap(undefined, this.config); for (const resource of orderedResources) { map.set(resource, undefined); } @@ -347,7 +352,8 @@ export default class BufferSyncSupport extends Disposable { constructor( client: ITypeScriptServiceClient, - modeIds: readonly string[] + modeIds: readonly string[], + onCaseInsenitiveFileSystem: boolean ) { super(); this.client = client; @@ -356,9 +362,9 @@ export default class BufferSyncSupport extends Disposable { this.diagnosticDelayer = new Delayer(300); const pathNormalizer = (path: vscode.Uri) => this.client.normalizedPath(path); - this.syncedBuffers = new SyncedBufferMap(pathNormalizer); - this.pendingDiagnostics = new PendingDiagnostics(pathNormalizer); - this.synchronizer = new BufferSynchronizer(client); + this.syncedBuffers = new SyncedBufferMap(pathNormalizer, { onCaseInsenitiveFileSystem }); + this.pendingDiagnostics = new PendingDiagnostics(pathNormalizer, { onCaseInsenitiveFileSystem }); + this.synchronizer = new BufferSynchronizer(client, onCaseInsenitiveFileSystem); this.updateConfiguration(); vscode.workspace.onDidChangeConfiguration(this.updateConfiguration, this, this._disposables); diff --git a/extensions/typescript-language-features/src/tsServer/cancellation.electron.ts b/extensions/typescript-language-features/src/tsServer/cancellation.electron.ts new file mode 100644 index 00000000000..853ca0c1594 --- /dev/null +++ b/extensions/typescript-language-features/src/tsServer/cancellation.electron.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; +import { getTempFile } from '../utils/temp.electron'; +import Tracer from '../utils/tracer'; +import { OngoingRequestCanceller, OngoingRequestCancellerFactory } from './cancellation'; + +export class NodeRequestCanceller implements OngoingRequestCanceller { + public readonly cancellationPipeName: string; + + public constructor( + private readonly _serverId: string, + private readonly _tracer: Tracer, + ) { + this.cancellationPipeName = getTempFile('tscancellation'); + } + + public tryCancelOngoingRequest(seq: number): boolean { + if (!this.cancellationPipeName) { + return false; + } + this._tracer.logTrace(this._serverId, `TypeScript Server: trying to cancel ongoing request with sequence number ${seq}`); + try { + fs.writeFileSync(this.cancellationPipeName + seq, ''); + } catch { + // noop + } + return true; + } +} + + +export const nodeRequestCancellerFactory = new class implements OngoingRequestCancellerFactory { + create(serverId: string, tracer: Tracer): OngoingRequestCanceller { + return new NodeRequestCanceller(serverId, tracer); + } +}; diff --git a/extensions/typescript-language-features/src/tsServer/cancellation.ts b/extensions/typescript-language-features/src/tsServer/cancellation.ts new file mode 100644 index 00000000000..0eda4e574dc --- /dev/null +++ b/extensions/typescript-language-features/src/tsServer/cancellation.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import Tracer from '../utils/tracer'; + +export interface OngoingRequestCanceller { + readonly cancellationPipeName: string | undefined; + tryCancelOngoingRequest(seq: number): boolean; +} + +export interface OngoingRequestCancellerFactory { + create(serverId: string, tracer: Tracer): OngoingRequestCanceller; +} + +const noopRequestCanceller = new class implements OngoingRequestCanceller { + public readonly cancellationPipeName = undefined; + + public tryCancelOngoingRequest(_seq: number): boolean { + return false; + } +}; + +export const noopRequestCancellerFactory = new class implements OngoingRequestCancellerFactory { + create(_serverId: string, _tracer: Tracer): OngoingRequestCanceller { + return noopRequestCanceller; + } +}; diff --git a/extensions/typescript-language-features/src/utils/logDirectoryProvider.ts b/extensions/typescript-language-features/src/tsServer/logDirectoryProvider.electron.ts similarity index 84% rename from extensions/typescript-language-features/src/utils/logDirectoryProvider.ts rename to extensions/typescript-language-features/src/tsServer/logDirectoryProvider.electron.ts index af6886e7043..60b6965b5a7 100644 --- a/extensions/typescript-language-features/src/utils/logDirectoryProvider.ts +++ b/extensions/typescript-language-features/src/tsServer/logDirectoryProvider.electron.ts @@ -6,9 +6,10 @@ import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; -import { memoize } from './memoize'; +import { ILogDirectoryProvider } from './logDirectoryProvider'; +import { memoize } from '../utils/memoize'; -export default class LogDirectoryProvider { +export class NodeLogDirectoryProvider implements ILogDirectoryProvider { public constructor( private readonly context: vscode.ExtensionContext ) { } diff --git a/extensions/typescript-language-features/src/tsServer/logDirectoryProvider.ts b/extensions/typescript-language-features/src/tsServer/logDirectoryProvider.ts new file mode 100644 index 00000000000..75ef2316309 --- /dev/null +++ b/extensions/typescript-language-features/src/tsServer/logDirectoryProvider.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface ILogDirectoryProvider { + getNewLogDirectory(): string | undefined; +} + +export const noopLogDirectoryProvider = new class implements ILogDirectoryProvider { + public getNewLogDirectory(): undefined { + return undefined; + } +}; diff --git a/extensions/typescript-language-features/src/tsServer/server.ts b/extensions/typescript-language-features/src/tsServer/server.ts index 55a14642a1f..fc7841322bd 100644 --- a/extensions/typescript-language-features/src/tsServer/server.ts +++ b/extensions/typescript-language-features/src/tsServer/server.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import * as vscode from 'vscode'; import type * as Proto from '../protocol'; import { EventName } from '../protocol.const'; @@ -11,34 +10,17 @@ import { CallbackMap } from '../tsServer/callbackMap'; import { RequestItem, RequestQueue, RequestQueueingType } from '../tsServer/requestQueue'; import { TypeScriptServerError } from '../tsServer/serverError'; import { ServerResponse, TypeScriptRequests } from '../typescriptService'; +import { TypeScriptServiceConfiguration } from '../utils/configuration'; import { Disposable } from '../utils/dispose'; import { TelemetryReporter } from '../utils/telemetry'; import Tracer from '../utils/tracer'; -import { TypeScriptVersion } from '../utils/versionProvider'; +import { OngoingRequestCanceller } from './cancellation'; +import { TypeScriptVersionManager } from './versionManager'; +import { TypeScriptVersion } from './versionProvider'; -export interface OngoingRequestCanceller { - tryCancelOngoingRequest(seq: number): boolean; -} - -export class PipeRequestCanceller implements OngoingRequestCanceller { - public constructor( - private readonly _serverId: string, - private readonly _cancellationPipeName: string | undefined, - private readonly _tracer: Tracer, - ) { } - - public tryCancelOngoingRequest(seq: number): boolean { - if (!this._cancellationPipeName) { - return false; - } - this._tracer.logTrace(this._serverId, `TypeScript Server: trying to cancel ongoing request with sequence number ${seq}`); - try { - fs.writeFileSync(this._cancellationPipeName + seq, ''); - } catch { - // noop - } - return true; - } +export enum ExectuionTarget { + Semantic, + Syntax } export interface ITypeScriptServer { @@ -50,9 +32,9 @@ export interface ITypeScriptServer { kill(): void; - executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined; - executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise>; - executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise> | undefined; + executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean, executionTarget?: ExectuionTarget }): undefined; + executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise>; + executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise> | undefined; dispose(): void; } @@ -61,6 +43,23 @@ export interface TsServerDelegate { onFatalError(command: string, error: Error): void; } +export const enum TsServerProcessKind { + Main = 'main', + Syntax = 'syntax', + Semantic = 'semantic', + Diagnostics = 'diagnostics' +} + +export interface TsServerProcessFactory { + fork( + tsServerPath: string, + args: readonly string[], + kind: TsServerProcessKind, + configuration: TypeScriptServiceConfiguration, + versionManager: TypeScriptVersionManager, + ): TsServerProcess; +} + export interface TsServerProcess { write(serverRequest: Proto.Request): void; @@ -71,7 +70,6 @@ export interface TsServerProcess { kill(): void; } - export class ProcessBasedTsServer extends Disposable implements ITypeScriptServer { private readonly _requestQueue = new RequestQueue(); private readonly _callbacks = new CallbackMap(); @@ -196,9 +194,9 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe } } - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined; - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise>; - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise> | undefined { + public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean, executionTarget?: ExectuionTarget }): undefined; + public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise>; + public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise> | undefined { const request = this._requestQueue.createRequest(command, args); const requestInfo: RequestItem = { request, @@ -296,6 +294,14 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe } +interface ExecuteInfo { + readonly isAsync: boolean; + readonly token?: vscode.CancellationToken; + readonly expectsResult: boolean; + readonly lowPriority?: boolean; + readonly executionTarget?: ExectuionTarget; +} + class RequestRouter { private static readonly sharedCommands = new Set([ @@ -308,13 +314,16 @@ class RequestRouter { ]); constructor( - private readonly servers: ReadonlyArray<{ readonly server: ITypeScriptServer, canRun?(command: keyof TypeScriptRequests): void }>, + private readonly servers: ReadonlyArray<{ + readonly server: ITypeScriptServer; + canRun?(command: keyof TypeScriptRequests, executeInfo: ExecuteInfo): void; + }>, private readonly delegate: TsServerDelegate, ) { } - public execute(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise> | undefined { - if (RequestRouter.sharedCommands.has(command)) { - // Dispatch shared commands to all server but only return from first one one + public execute(command: keyof TypeScriptRequests, args: any, executeInfo: ExecuteInfo): Promise> | undefined { + if (RequestRouter.sharedCommands.has(command) && typeof executeInfo.executionTarget === 'undefined') { + // Dispatch shared commands to all servers but only return from first one const requestStates: RequestState.State[] = this.servers.map(() => RequestState.Unresolved); @@ -368,7 +377,7 @@ class RequestRouter { } for (const { canRun, server } of this.servers) { - if (!canRun || canRun(command)) { + if (!canRun || canRun(command, executeInfo)) { return server.executeImpl(command, args, executeInfo); } } @@ -444,9 +453,9 @@ export class GetErrRoutingTsServer extends Disposable implements ITypeScriptServ this.mainServer.kill(); } - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined; - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise>; - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise> | undefined { + public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean, executionTarget?: ExectuionTarget }): undefined; + public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise>; + public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise> | undefined { return this.router.execute(command, args, executeInfo); } } @@ -514,7 +523,12 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ [ { server: this.syntaxServer, - canRun: (command) => { + canRun: (command, execInfo) => { + switch (execInfo.executionTarget) { + case ExectuionTarget.Semantic: return false; + case ExectuionTarget.Syntax: return true; + } + if (SyntaxRoutingTsServer.syntaxAlwaysCommands.has(command)) { return true; } @@ -580,9 +594,9 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ this.semanticServer.kill(); } - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined; - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise>; - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise> | undefined { + public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean, executionTarget?: ExectuionTarget }): undefined; + public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise>; + public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise> | undefined { return this.router.execute(command, args, executeInfo); } } diff --git a/extensions/typescript-language-features/src/tsServer/serverError.ts b/extensions/typescript-language-features/src/tsServer/serverError.ts index 643998cd4ec..42622669498 100644 --- a/extensions/typescript-language-features/src/tsServer/serverError.ts +++ b/extensions/typescript-language-features/src/tsServer/serverError.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type * as Proto from '../protocol'; -import { TypeScriptVersion } from '../utils/versionProvider'; +import { TypeScriptVersion } from './versionProvider'; export class TypeScriptServerError extends Error { @@ -18,7 +18,7 @@ export class TypeScriptServerError extends Error { } private constructor( - serverId: string, + public readonly serverId: string, public readonly version: TypeScriptVersion, private readonly response: Proto.Response, public readonly serverMessage: string | undefined, @@ -38,11 +38,13 @@ export class TypeScriptServerError extends Error { /* __GDPR__FRAGMENT__ "TypeScriptRequestErrorProperties" : { "command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "sanitizedstack" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + "serverid" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "sanitizedstack" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, } */ return { command: this.serverCommand, + serverid: this.serverId, sanitizedstack: this.sanitizedStack || '', } as const; } diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts new file mode 100644 index 00000000000..01071f6e10e --- /dev/null +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as Proto from '../protocol'; +import { TypeScriptServiceConfiguration } from '../utils/configuration'; +import { TsServerProcess, TsServerProcessKind } from './server'; + +declare const Worker: any; +declare type Worker = any; + +export class WorkerServerProcess implements TsServerProcess { + + public static fork( + tsServerPath: string, + args: readonly string[], + _kind: TsServerProcessKind, + _configuration: TypeScriptServiceConfiguration, + ) { + const worker = new Worker(tsServerPath); + return new WorkerServerProcess(worker, args); + } + + private _onDataHandlers = new Set<(data: Proto.Response) => void>(); + private _onErrorHandlers = new Set<(err: Error) => void>(); + private _onExitHandlers = new Set<(code: number | null) => void>(); + + public constructor( + private readonly worker: Worker, + args: readonly string[], + ) { + worker.addEventListener('message', (msg: any) => { + for (const handler of this._onDataHandlers) { + handler(msg.data); + } + }); + worker.postMessage(args); + } + + write(serverRequest: Proto.Request): void { + this.worker.postMessage(serverRequest); + } + + onData(handler: (response: Proto.Response) => void): void { + this._onDataHandlers.add(handler); + } + + onError(handler: (err: Error) => void): void { + this._onErrorHandlers.add(handler); + // Todo: not implemented + } + + onExit(handler: (code: number | null) => void): void { + this._onExitHandlers.add(handler); + // Todo: not implemented + } + + kill(): void { + this.worker.terminate(); + } +} diff --git a/extensions/typescript-language-features/src/utils/serverProcess.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts similarity index 64% rename from extensions/typescript-language-features/src/utils/serverProcess.ts rename to extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts index 495731f34a1..f9d70d858e0 100644 --- a/extensions/typescript-language-features/src/utils/serverProcess.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts @@ -3,12 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ChildProcess } from 'child_process'; -import * as stream from 'stream'; +import * as child_process from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import type { Readable } from 'stream'; import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; import type * as Proto from '../protocol'; -import { TsServerProcess } from '../tsServer/server'; -import { Disposable } from './dispose'; +import { TypeScriptServiceConfiguration } from '../utils/configuration'; +import { Disposable } from '../utils/dispose'; +import { TsServerProcess, TsServerProcessKind } from './server'; +import { TypeScriptVersionManager } from './versionManager'; + +const localize = nls.loadMessageBundle(); const defaultSize: number = 8192; const contentLength: string = 'Content-Length: '; @@ -88,7 +95,7 @@ class Reader extends Disposable { private readonly buffer: ProtocolBuffer = new ProtocolBuffer(); private nextMessageLength: number = -1; - public constructor(readable: stream.Readable) { + public constructor(readable: Readable) { super(); readable.on('data', data => this.onLengthData(data)); } @@ -130,8 +137,67 @@ class Reader extends Disposable { export class ChildServerProcess extends Disposable implements TsServerProcess { private readonly _reader: Reader; - public constructor( - private readonly _process: ChildProcess, + public static fork( + tsServerPath: string, + args: readonly string[], + kind: TsServerProcessKind, + configuration: TypeScriptServiceConfiguration, + versionManager: TypeScriptVersionManager, + ): ChildServerProcess { + if (!fs.existsSync(tsServerPath)) { + vscode.window.showWarningMessage(localize('noServerFound', 'The path {0} doesn\'t point to a valid tsserver install. Falling back to bundled TypeScript version.', tsServerPath)); + versionManager.reset(); + tsServerPath = versionManager.currentVersion.tsServerPath; + } + + const childProcess = child_process.fork(tsServerPath, args, { + silent: true, + cwd: undefined, + env: this.generatePatchedEnv(process.env, tsServerPath), + execArgv: this.getExecArgv(kind, configuration), + }); + + return new ChildServerProcess(childProcess); + } + + private static generatePatchedEnv(env: any, modulePath: string): any { + const newEnv = Object.assign({}, env); + + newEnv['ELECTRON_RUN_AS_NODE'] = '1'; + newEnv['NODE_PATH'] = path.join(modulePath, '..', '..', '..'); + + // Ensure we always have a PATH set + newEnv['PATH'] = newEnv['PATH'] || process.env.PATH; + + return newEnv; + } + + private static getExecArgv(kind: TsServerProcessKind, configuration: TypeScriptServiceConfiguration): 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}`] : []) + ]; + } + + private static getDebugPort(kind: TsServerProcessKind): number | undefined { + if (kind === TsServerProcessKind.Syntax) { + // We typically only want to debug the main semantic server + return undefined; + } + const value = process.env['TSS_DEBUG_BRK'] || process.env['TSS_DEBUG']; + if (value) { + const port = parseInt(value); + if (!isNaN(port)) { + return port; + } + } + return undefined; + } + + private constructor( + private readonly _process: child_process.ChildProcess, ) { super(); this._reader = this._register(new Reader(this._process.stdout!)); diff --git a/extensions/typescript-language-features/src/tsServer/spawner.ts b/extensions/typescript-language-features/src/tsServer/spawner.ts index 5bc505d0bf6..092ad4f0fb6 100644 --- a/extensions/typescript-language-features/src/tsServer/spawner.ts +++ b/extensions/typescript-language-features/src/tsServer/spawner.ts @@ -5,26 +5,20 @@ import * as path from 'path'; import * as vscode from 'vscode'; +import { OngoingRequestCancellerFactory } from '../tsServer/cancellation'; import { ClientCapabilities, ClientCapability } from '../typescriptService'; import API from '../utils/api'; import { SeparateSyntaxServerConfiguration, TsServerLogLevel, TypeScriptServiceConfiguration } from '../utils/configuration'; -import * as electron from '../utils/electron'; -import LogDirectoryProvider from '../utils/logDirectoryProvider'; -import Logger from '../utils/logger'; +import { Logger } from '../utils/logger'; import { TypeScriptPluginPathsProvider } from '../utils/pluginPathsProvider'; import { PluginManager } from '../utils/plugins'; -import { ChildServerProcess } from '../utils/serverProcess'; import { TelemetryReporter } from '../utils/telemetry'; import Tracer from '../utils/tracer'; -import { TypeScriptVersion, TypeScriptVersionProvider } from '../utils/versionProvider'; -import { GetErrRoutingTsServer, ITypeScriptServer, PipeRequestCanceller, ProcessBasedTsServer, SyntaxRoutingTsServer, TsServerDelegate } from './server'; - -const enum ServerKind { - Main = 'main', - Syntax = 'syntax', - Semantic = 'semantic', - Diagnostics = 'diagnostics' -} +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 */ @@ -42,12 +36,14 @@ const enum CompositeServerType { export class TypeScriptServerSpawner { public constructor( - private readonly _versionProvider: TypeScriptVersionProvider, - private readonly _logDirectoryProvider: LogDirectoryProvider, + private readonly _versionProvider: ITypeScriptVersionProvider, + private readonly _versionManager: TypeScriptVersionManager, + private readonly _logDirectoryProvider: ILogDirectoryProvider, private readonly _pluginPathsProvider: TypeScriptPluginPathsProvider, private readonly _logger: Logger, private readonly _telemetryReporter: TelemetryReporter, private readonly _tracer: Tracer, + private readonly _factory: TsServerProcessFactory, ) { } public spawn( @@ -55,6 +51,7 @@ export class TypeScriptServerSpawner { capabilities: ClientCapabilities, configuration: TypeScriptServiceConfiguration, pluginManager: PluginManager, + cancellerFactory: OngoingRequestCancellerFactory, delegate: TsServerDelegate, ): ITypeScriptServer { let primaryServer: ITypeScriptServer; @@ -65,26 +62,26 @@ export class TypeScriptServerSpawner { { const enableDynamicRouting = serverType === CompositeServerType.DynamicSeparateSyntax; primaryServer = new SyntaxRoutingTsServer({ - syntax: this.spawnTsServer(ServerKind.Syntax, version, configuration, pluginManager), - semantic: this.spawnTsServer(ServerKind.Semantic, version, configuration, pluginManager) + syntax: this.spawnTsServer(TsServerProcessKind.Syntax, version, configuration, pluginManager, cancellerFactory), + semantic: this.spawnTsServer(TsServerProcessKind.Semantic, version, configuration, pluginManager, cancellerFactory), }, delegate, enableDynamicRouting); break; } case CompositeServerType.Single: { - primaryServer = this.spawnTsServer(ServerKind.Main, version, configuration, pluginManager); + primaryServer = this.spawnTsServer(TsServerProcessKind.Main, version, configuration, pluginManager, cancellerFactory); break; } case CompositeServerType.SyntaxOnly: { - primaryServer = this.spawnTsServer(ServerKind.Syntax, version, configuration, pluginManager); + primaryServer = this.spawnTsServer(TsServerProcessKind.Syntax, version, configuration, pluginManager, cancellerFactory); break; } } if (this.shouldUseSeparateDiagnosticsServer(configuration)) { return new GetErrRoutingTsServer({ - getErr: this.spawnTsServer(ServerKind.Diagnostics, version, configuration, pluginManager), + getErr: this.spawnTsServer(TsServerProcessKind.Diagnostics, version, configuration, pluginManager, cancellerFactory), primary: primaryServer, }, delegate); } @@ -122,14 +119,16 @@ export class TypeScriptServerSpawner { } private spawnTsServer( - kind: ServerKind, + kind: TsServerProcessKind, version: TypeScriptVersion, configuration: TypeScriptServiceConfiguration, pluginManager: PluginManager, + cancellerFactory: OngoingRequestCancellerFactory, ): ITypeScriptServer { const apiVersion = version.apiVersion || API.defaultVersion; - const { args, cancellationPipeName, tsServerLogFile } = this.getTsServerArgs(kind, configuration, version, apiVersion, pluginManager); + const canceller = cancellerFactory.create(kind, this._tracer); + const { args, tsServerLogFile } = this.getTsServerArgs(kind, configuration, version, apiVersion, pluginManager, canceller.cancellationPipeName); if (TypeScriptServerSpawner.isLoggingEnabled(configuration)) { if (tsServerLogFile) { @@ -140,43 +139,38 @@ export class TypeScriptServerSpawner { } this._logger.info(`<${kind}> Forking...`); - const childProcess = electron.fork(version.tsServerPath, args, this.getForkOptions(kind, configuration)); + const process = this._factory.fork(version.tsServerPath, args, kind, configuration, this._versionManager); this._logger.info(`<${kind}> Starting...`); return new ProcessBasedTsServer( kind, - new ChildServerProcess(childProcess), + process!, tsServerLogFile, - new PipeRequestCanceller(kind, cancellationPipeName, this._tracer), + canceller, version, this._telemetryReporter, this._tracer); } - private getForkOptions(kind: ServerKind, configuration: TypeScriptServiceConfiguration) { - const debugPort = TypeScriptServerSpawner.getDebugPort(kind); - const inspectFlag = process.env['TSS_DEBUG_BRK'] ? '--inspect-brk' : '--inspect'; - const tsServerForkOptions: electron.ForkOptions = { - execArgv: [ - ...(debugPort ? [`${inspectFlag}=${debugPort}`] : []), - ...(configuration.maxTsServerMemory ? [`--max-old-space-size=${configuration.maxTsServerMemory}`] : []) - ] - }; - return tsServerForkOptions; - } - private getTsServerArgs( - kind: ServerKind, + kind: TsServerProcessKind, configuration: TypeScriptServiceConfiguration, currentVersion: TypeScriptVersion, apiVersion: API, pluginManager: PluginManager, - ): { args: string[], cancellationPipeName: string, tsServerLogFile: string | undefined } { + cancellationPipeName: string | undefined, + ): { args: string[], tsServerLogFile: string | undefined } { const args: string[] = []; let tsServerLogFile: string | undefined; - if (kind === ServerKind.Syntax) { - args.push('--syntaxOnly'); + if (kind === TsServerProcessKind.Syntax) { + if (semver.gte(API.v400rc.fullVersionString, apiVersion.fullVersionString)) { + args.push('--serverMode'); + args.push('partialSemantic'); + } + else { + args.push('--syntaxOnly'); + } } if (apiVersion.gte(API.v250)) { @@ -185,16 +179,17 @@ export class TypeScriptServerSpawner { args.push('--useSingleInferredProject'); } - if (configuration.disableAutomaticTypeAcquisition || kind === ServerKind.Syntax || kind === ServerKind.Diagnostics) { + if (configuration.disableAutomaticTypeAcquisition || kind === TsServerProcessKind.Syntax || kind === TsServerProcessKind.Diagnostics) { args.push('--disableAutomaticTypingAcquisition'); } - if (kind === ServerKind.Semantic || kind === ServerKind.Main) { + if (kind === TsServerProcessKind.Semantic || kind === TsServerProcessKind.Main) { args.push('--enableTelemetry'); } - const cancellationPipeName = electron.getTempFile('tscancellation'); - args.push('--cancellationPipeName', cancellationPipeName + '*'); + if (cancellationPipeName) { + args.push('--cancellationPipeName', cancellationPipeName + '*'); + } if (TypeScriptServerSpawner.isLoggingEnabled(configuration)) { const logDir = this._logDirectoryProvider.getNewLogDirectory(); @@ -238,22 +233,7 @@ export class TypeScriptServerSpawner { args.push('--validateDefaultNpmLocation'); } - return { args, cancellationPipeName, tsServerLogFile }; - } - - private static getDebugPort(kind: ServerKind): number | undefined { - if (kind === 'syntax') { - // We typically only want to debug the main semantic server - return undefined; - } - const value = process.env['TSS_DEBUG_BRK'] || process.env['TSS_DEBUG']; - if (value) { - const port = parseInt(value); - if (!isNaN(port)) { - return port; - } - } - return undefined; + return { args, tsServerLogFile }; } private static isLoggingEnabled(configuration: TypeScriptServiceConfiguration) { diff --git a/extensions/typescript-language-features/src/utils/versionManager.ts b/extensions/typescript-language-features/src/tsServer/versionManager.ts similarity index 96% rename from extensions/typescript-language-features/src/utils/versionManager.ts rename to extensions/typescript-language-features/src/tsServer/versionManager.ts index 08f31787e40..4811fefd3a7 100644 --- a/extensions/typescript-language-features/src/utils/versionManager.ts +++ b/extensions/typescript-language-features/src/tsServer/versionManager.ts @@ -5,9 +5,9 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import { TypeScriptVersion, TypeScriptVersionProvider } from './versionProvider'; -import { Disposable } from './dispose'; import { TypeScriptServiceConfiguration } from '../utils/configuration'; +import { Disposable } from '../utils/dispose'; +import { ITypeScriptVersionProvider, TypeScriptVersion } from './versionProvider'; const localize = nls.loadMessageBundle(); @@ -24,7 +24,7 @@ export class TypeScriptVersionManager extends Disposable { public constructor( private configuration: TypeScriptServiceConfiguration, - private readonly versionProvider: TypeScriptVersionProvider, + private readonly versionProvider: ITypeScriptVersionProvider, private readonly workspaceState: vscode.Memento ) { super(); diff --git a/extensions/typescript-language-features/src/utils/versionProvider.ts b/extensions/typescript-language-features/src/tsServer/versionProvider.electron.ts similarity index 69% rename from extensions/typescript-language-features/src/utils/versionProvider.ts rename to extensions/typescript-language-features/src/tsServer/versionProvider.electron.ts index 742efda6d28..15107bc4b5b 100644 --- a/extensions/typescript-language-features/src/utils/versionProvider.ts +++ b/extensions/typescript-language-features/src/tsServer/versionProvider.electron.ts @@ -2,70 +2,151 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; -import * as nls from 'vscode-nls'; -import API from './api'; -import { TypeScriptServiceConfiguration } from './configuration'; -import { RelativeWorkspacePathResolver } from './relativePathResolver'; +import API from '../utils/api'; +import { TypeScriptServiceConfiguration } from '../utils/configuration'; +import { RelativeWorkspacePathResolver } from '../utils/relativePathResolver'; +import { ITypeScriptVersionProvider, localize, TypeScriptVersion, TypeScriptVersionSource } from './versionProvider'; -const localize = nls.loadMessageBundle(); +export class DiskTypeScriptVersionProvider implements ITypeScriptVersionProvider { -const enum TypeScriptVersionSource { - Bundled = 'bundled', - TsNightlyExtension = 'ts-nightly-extension', - NodeModules = 'node-modules', - UserSetting = 'user-setting', - WorkspaceSetting = 'workspace-setting', -} + public constructor( + private configuration?: TypeScriptServiceConfiguration + ) { } -export class TypeScriptVersion { - - public readonly apiVersion: API | undefined; - - constructor( - public readonly source: TypeScriptVersionSource, - public readonly path: string, - private readonly _pathLabel?: string - ) { - this.apiVersion = TypeScriptVersion.getApiVersion(this.tsServerPath); + public updateConfiguration(configuration: TypeScriptServiceConfiguration): void { + this.configuration = configuration; } - public get tsServerPath(): string { - return path.join(this.path, 'tsserver.js'); + public get defaultVersion(): TypeScriptVersion { + return this.globalVersion || this.bundledVersion; } - public get pathLabel(): string { - return this._pathLabel ?? this.path; + public get globalVersion(): TypeScriptVersion | undefined { + if (this.configuration?.globalTsdk) { + const globals = this.loadVersionsFromSetting(TypeScriptVersionSource.UserSetting, this.configuration.globalTsdk); + if (globals && globals.length) { + return globals[0]; + } + } + return this.contributedTsNextVersion; } - public get isValid(): boolean { - return this.apiVersion !== undefined; - } - - public eq(other: TypeScriptVersion): boolean { - if (this.path !== other.path) { - return false; + public get localVersion(): TypeScriptVersion | undefined { + const tsdkVersions = this.localTsdkVersions; + if (tsdkVersions && tsdkVersions.length) { + return tsdkVersions[0]; } - if (this.apiVersion === other.apiVersion) { + const nodeVersions = this.localNodeModulesVersions; + if (nodeVersions && nodeVersions.length === 1) { + return nodeVersions[0]; + } + return undefined; + } + + + public get localVersions(): TypeScriptVersion[] { + const allVersions = this.localTsdkVersions.concat(this.localNodeModulesVersions); + const paths = new Set(); + return allVersions.filter(x => { + if (paths.has(x.path)) { + return false; + } + paths.add(x.path); return true; - } - if (!this.apiVersion || !other.apiVersion) { - return false; - } - return this.apiVersion.eq(other.apiVersion); + }); } - public get displayName(): string { - const version = this.apiVersion; - return version ? version.displayName : localize( - 'couldNotLoadTsVersion', 'Could not load the TypeScript version at this path'); + public get bundledVersion(): TypeScriptVersion { + const version = this.getContributedVersion(TypeScriptVersionSource.Bundled, 'vscode.typescript-language-features', ['..', 'node_modules']); + if (version) { + return version; + } + + vscode.window.showErrorMessage(localize( + 'noBundledServerFound', + 'VS Code\'s tsserver was deleted by another application such as a misbehaving virus detection tool. Please reinstall VS Code.')); + throw new Error('Could not find bundled tsserver.js'); } - public static getApiVersion(serverPath: string): API | undefined { - const version = TypeScriptVersion.getTypeScriptVersion(serverPath); + private get contributedTsNextVersion(): TypeScriptVersion | undefined { + return this.getContributedVersion(TypeScriptVersionSource.TsNightlyExtension, 'ms-vscode.vscode-typescript-next', ['node_modules']); + } + + private getContributedVersion(source: TypeScriptVersionSource, extensionId: string, pathToTs: readonly string[]): TypeScriptVersion | undefined { + try { + const extension = vscode.extensions.getExtension(extensionId); + if (extension) { + const serverPath = path.join(extension.extensionPath, ...pathToTs, 'typescript', 'lib', 'tsserver.js'); + const bundledVersion = new TypeScriptVersion(source, serverPath, DiskTypeScriptVersionProvider.getApiVersion(serverPath), ''); + if (bundledVersion.isValid) { + return bundledVersion; + } + } + } catch { + // noop + } + return undefined; + } + + private get localTsdkVersions(): TypeScriptVersion[] { + const localTsdk = this.configuration?.localTsdk; + return localTsdk ? this.loadVersionsFromSetting(TypeScriptVersionSource.WorkspaceSetting, localTsdk) : []; + } + + private loadVersionsFromSetting(source: TypeScriptVersionSource, tsdkPathSetting: string): TypeScriptVersion[] { + if (path.isAbsolute(tsdkPathSetting)) { + const serverPath = path.join(tsdkPathSetting, 'tsserver.js'); + return [ + new TypeScriptVersion(source, + serverPath, + DiskTypeScriptVersionProvider.getApiVersion(serverPath)) + ]; + } + + const workspacePath = RelativeWorkspacePathResolver.asAbsoluteWorkspacePath(tsdkPathSetting); + if (workspacePath !== undefined) { + const serverPath = path.join(workspacePath, 'tsserver.js'); + return [ + new TypeScriptVersion(source, + serverPath, + DiskTypeScriptVersionProvider.getApiVersion(serverPath), + tsdkPathSetting) + ]; + } + + return this.loadTypeScriptVersionsFromPath(source, tsdkPathSetting); + } + + private get localNodeModulesVersions(): TypeScriptVersion[] { + return this.loadTypeScriptVersionsFromPath(TypeScriptVersionSource.NodeModules, path.join('node_modules', 'typescript', 'lib')) + .filter(x => x.isValid); + } + + private loadTypeScriptVersionsFromPath(source: TypeScriptVersionSource, relativePath: string): TypeScriptVersion[] { + if (!vscode.workspace.workspaceFolders) { + return []; + } + + const versions: TypeScriptVersion[] = []; + for (const root of vscode.workspace.workspaceFolders) { + let label: string = relativePath; + if (vscode.workspace.workspaceFolders.length > 1) { + label = path.join(root.name, relativePath); + } + + const serverPath = path.join(root.uri.fsPath, relativePath, 'tsserver.js'); + versions.push(new TypeScriptVersion(source, serverPath, DiskTypeScriptVersionProvider.getApiVersion(serverPath), label)); + } + return versions; + } + + private static getApiVersion(serverPath: string): API | undefined { + const version = DiskTypeScriptVersionProvider.getTypeScriptVersion(serverPath); if (version) { return version; } @@ -114,125 +195,3 @@ export class TypeScriptVersion { return desc.version ? API.fromVersionString(desc.version) : undefined; } } - -export class TypeScriptVersionProvider { - - public constructor( - private configuration: TypeScriptServiceConfiguration - ) { } - - public updateConfiguration(configuration: TypeScriptServiceConfiguration): void { - this.configuration = configuration; - } - - public get defaultVersion(): TypeScriptVersion { - return this.globalVersion || this.bundledVersion; - } - - public get globalVersion(): TypeScriptVersion | undefined { - if (this.configuration.globalTsdk) { - const globals = this.loadVersionsFromSetting(TypeScriptVersionSource.UserSetting, this.configuration.globalTsdk); - if (globals && globals.length) { - return globals[0]; - } - } - return this.contributedTsNextVersion; - } - - public get localVersion(): TypeScriptVersion | undefined { - const tsdkVersions = this.localTsdkVersions; - if (tsdkVersions && tsdkVersions.length) { - return tsdkVersions[0]; - } - - const nodeVersions = this.localNodeModulesVersions; - if (nodeVersions && nodeVersions.length === 1) { - return nodeVersions[0]; - } - return undefined; - } - - public get localVersions(): TypeScriptVersion[] { - const allVersions = this.localTsdkVersions.concat(this.localNodeModulesVersions); - const paths = new Set(); - return allVersions.filter(x => { - if (paths.has(x.path)) { - return false; - } - paths.add(x.path); - return true; - }); - } - - public get bundledVersion(): TypeScriptVersion { - const version = this.getContributedVersion(TypeScriptVersionSource.Bundled, 'vscode.typescript-language-features', ['..', 'node_modules']); - if (version) { - return version; - } - - vscode.window.showErrorMessage(localize( - 'noBundledServerFound', - 'VS Code\'s tsserver was deleted by another application such as a misbehaving virus detection tool. Please reinstall VS Code.')); - throw new Error('Could not find bundled tsserver.js'); - } - - private get contributedTsNextVersion(): TypeScriptVersion | undefined { - return this.getContributedVersion(TypeScriptVersionSource.TsNightlyExtension, 'ms-vscode.vscode-typescript-next', ['node_modules']); - } - - private getContributedVersion(source: TypeScriptVersionSource, extensionId: string, pathToTs: readonly string[]): TypeScriptVersion | undefined { - try { - const extension = vscode.extensions.getExtension(extensionId); - if (extension) { - const typescriptPath = path.join(extension.extensionPath, ...pathToTs, 'typescript', 'lib'); - const bundledVersion = new TypeScriptVersion(source, typescriptPath, ''); - if (bundledVersion.isValid) { - return bundledVersion; - } - } - } catch { - // noop - } - return undefined; - } - - private get localTsdkVersions(): TypeScriptVersion[] { - const localTsdk = this.configuration.localTsdk; - return localTsdk ? this.loadVersionsFromSetting(TypeScriptVersionSource.WorkspaceSetting, localTsdk) : []; - } - - private loadVersionsFromSetting(source: TypeScriptVersionSource, tsdkPathSetting: string): TypeScriptVersion[] { - if (path.isAbsolute(tsdkPathSetting)) { - return [new TypeScriptVersion(source, tsdkPathSetting)]; - } - - const workspacePath = RelativeWorkspacePathResolver.asAbsoluteWorkspacePath(tsdkPathSetting); - if (workspacePath !== undefined) { - return [new TypeScriptVersion(source, workspacePath, tsdkPathSetting)]; - } - - return this.loadTypeScriptVersionsFromPath(source, tsdkPathSetting); - } - - private get localNodeModulesVersions(): TypeScriptVersion[] { - return this.loadTypeScriptVersionsFromPath(TypeScriptVersionSource.NodeModules, path.join('node_modules', 'typescript', 'lib')) - .filter(x => x.isValid); - } - - private loadTypeScriptVersionsFromPath(source: TypeScriptVersionSource, relativePath: string): TypeScriptVersion[] { - if (!vscode.workspace.workspaceFolders) { - return []; - } - - const versions: TypeScriptVersion[] = []; - for (const root of vscode.workspace.workspaceFolders) { - let label: string = relativePath; - if (vscode.workspace.workspaceFolders.length > 1) { - label = path.join(root.name, relativePath); - } - - versions.push(new TypeScriptVersion(source, path.join(root.uri.fsPath, relativePath), label)); - } - return versions; - } -} diff --git a/extensions/typescript-language-features/src/tsServer/versionProvider.ts b/extensions/typescript-language-features/src/tsServer/versionProvider.ts new file mode 100644 index 00000000000..43f16c7c19d --- /dev/null +++ b/extensions/typescript-language-features/src/tsServer/versionProvider.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vscode-nls'; +import API from '../utils/api'; +import { TypeScriptServiceConfiguration } from '../utils/configuration'; + +export const localize = nls.loadMessageBundle(); + +export const enum TypeScriptVersionSource { + Bundled = 'bundled', + TsNightlyExtension = 'ts-nightly-extension', + NodeModules = 'node-modules', + UserSetting = 'user-setting', + WorkspaceSetting = 'workspace-setting', +} + +export class TypeScriptVersion { + + constructor( + public readonly source: TypeScriptVersionSource, + public readonly path: string, + public readonly apiVersion: API | undefined, + private readonly _pathLabel?: string, + ) { } + + public get tsServerPath(): string { + return this.path; + } + + public get pathLabel(): string { + return this._pathLabel ?? this.path; + } + + public get isValid(): boolean { + return this.apiVersion !== undefined; + } + + public eq(other: TypeScriptVersion): boolean { + if (this.path !== other.path) { + return false; + } + + if (this.apiVersion === other.apiVersion) { + return true; + } + if (!this.apiVersion || !other.apiVersion) { + return false; + } + return this.apiVersion.eq(other.apiVersion); + } + + public get displayName(): string { + const version = this.apiVersion; + return version ? version.displayName : localize( + 'couldNotLoadTsVersion', 'Could not load the TypeScript version at this path'); + } +} + +export interface ITypeScriptVersionProvider { + updateConfiguration(configuration: TypeScriptServiceConfiguration): void; + + readonly defaultVersion: TypeScriptVersion; + readonly globalVersion: TypeScriptVersion | undefined; + readonly localVersion: TypeScriptVersion | undefined; + readonly localVersions: readonly TypeScriptVersion[]; + readonly bundledVersion: TypeScriptVersion; +} diff --git a/extensions/typescript-language-features/src/utils/versionStatus.ts b/extensions/typescript-language-features/src/tsServer/versionStatus.ts similarity index 97% rename from extensions/typescript-language-features/src/utils/versionStatus.ts rename to extensions/typescript-language-features/src/tsServer/versionStatus.ts index 5f61eb23cc4..20b9debc27b 100644 --- a/extensions/typescript-language-features/src/utils/versionStatus.ts +++ b/extensions/typescript-language-features/src/tsServer/versionStatus.ts @@ -5,12 +5,12 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; +import { Command, CommandManager } from '../commands/commandManager'; import { ITypeScriptServiceClient } from '../typescriptService'; import { coalesce } from '../utils/arrays'; -import { Command, CommandManager } from '../utils/commandManager'; +import { Disposable } from '../utils/dispose'; import { isTypeScriptDocument } from '../utils/languageModeIds'; -import { isImplicitProjectConfigFile, openOrCreateConfig, openProjectConfigOrPromptToCreate, openProjectConfigForFile, ProjectType } from '../utils/tsconfig'; -import { Disposable } from './dispose'; +import { isImplicitProjectConfigFile, openOrCreateConfig, openProjectConfigForFile, openProjectConfigOrPromptToCreate, ProjectType } from '../utils/tsconfig'; import { TypeScriptVersion } from './versionProvider'; const localize = nls.loadMessageBundle(); diff --git a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts index ff06c82b96c..908735a7f2d 100644 --- a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts +++ b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts @@ -9,22 +9,26 @@ * ------------------------------------------------------------------------------------------ */ import * as vscode from 'vscode'; -import { DiagnosticKind } from './features/diagnostics'; -import FileConfigurationManager from './features/fileConfigurationManager'; +import { DiagnosticKind } from './languageFeatures/diagnostics'; +import FileConfigurationManager from './languageFeatures/fileConfigurationManager'; import LanguageProvider from './languageProvider'; import * as Proto from './protocol'; import * as PConst from './protocol.const'; +import { OngoingRequestCancellerFactory } from './tsServer/cancellation'; +import { ILogDirectoryProvider } from './tsServer/logDirectoryProvider'; +import { TsServerProcessFactory } from './tsServer/server'; +import { ITypeScriptVersionProvider } from './tsServer/versionProvider'; +import VersionStatus from './tsServer/versionStatus'; import TypeScriptServiceClient from './typescriptServiceClient'; import { coalesce, flatten } from './utils/arrays'; -import { CommandManager } from './utils/commandManager'; +import { CommandManager } from './commands/commandManager'; import { Disposable } from './utils/dispose'; import * as errorCodes from './utils/errorCodes'; import { DiagnosticLanguage, LanguageDescription } from './utils/languageDescription'; -import LogDirectoryProvider from './utils/logDirectoryProvider'; import { PluginManager } from './utils/plugins'; import * as typeConverters from './utils/typeConverters'; import TypingsStatus, { AtaProgressReporter } from './utils/typingsStatus'; -import VersionStatus from './utils/versionStatus'; +import * as ProjectStatus from './utils/largeProjectStatus'; namespace Experimental { export interface Diagnostic extends Proto.Diagnostic { @@ -55,21 +59,31 @@ export default class TypeScriptServiceClientHost extends Disposable { private reportStyleCheckAsWarnings: boolean = true; + private readonly commandManager: CommandManager; + constructor( descriptions: LanguageDescription[], workspaceState: vscode.Memento, - pluginManager: PluginManager, - private readonly commandManager: CommandManager, - logDirectoryProvider: LogDirectoryProvider, + onCaseInsenitiveFileSystem: boolean, + services: { + pluginManager: PluginManager, + commandManager: CommandManager, + logDirectoryProvider: ILogDirectoryProvider, + cancellerFactory: OngoingRequestCancellerFactory, + versionProvider: ITypeScriptVersionProvider, + processFactory: TsServerProcessFactory, + }, onCompletionAccepted: (item: vscode.CompletionItem) => void, ) { super(); - const allModeIds = this.getAllModeIds(descriptions, pluginManager); + this.commandManager = services.commandManager; + + const allModeIds = this.getAllModeIds(descriptions, services.pluginManager); this.client = this._register(new TypeScriptServiceClient( workspaceState, - pluginManager, - logDirectoryProvider, + onCaseInsenitiveFileSystem, + services, allModeIds)); this.client.onDiagnosticsReceived(({ kind, resource, diagnostics }) => { @@ -79,10 +93,12 @@ export default class TypeScriptServiceClientHost extends Disposable { this.client.onConfigDiagnosticsReceived(diag => this.configFileDiagnosticsReceived(diag), null, this._disposables); this.client.onResendModelsRequested(() => this.populateService(), null, this._disposables); - this._register(new VersionStatus(this.client, commandManager)); + this._register(new VersionStatus(this.client, services.commandManager)); this._register(new AtaProgressReporter(this.client)); this.typingsStatus = this._register(new TypingsStatus(this.client)); - this.fileConfigurationManager = this._register(new FileConfigurationManager(this.client)); + this._register(ProjectStatus.create(this.client)); + + this.fileConfigurationManager = this._register(new FileConfigurationManager(this.client, onCaseInsenitiveFileSystem)); for (const description of descriptions) { const manager = new LanguageProvider(this.client, description, this.commandManager, this.client.telemetryReporter, this.typingsStatus, this.fileConfigurationManager, onCompletionAccepted); @@ -91,16 +107,16 @@ export default class TypeScriptServiceClientHost extends Disposable { this.languagePerId.set(description.id, manager); } - import('./features/updatePathsOnRename').then(module => + import('./languageFeatures/updatePathsOnRename').then(module => this._register(module.register(this.client, this.fileConfigurationManager, uri => this.handles(uri)))); - import('./features/workspaceSymbols').then(module => + import('./languageFeatures/workspaceSymbols').then(module => this._register(module.register(this.client, allModeIds))); this.client.ensureServiceStarted(); this.client.onReady(() => { const languages = new Set(); - for (const plugin of pluginManager.plugins) { + for (const plugin of services.pluginManager.plugins) { if (plugin.configNamespace && plugin.languages.length) { this.registerExtensionLanguageProvider({ id: plugin.configNamespace, diff --git a/extensions/typescript-language-features/src/typescriptService.ts b/extensions/typescript-language-features/src/typescriptService.ts index 2c6ddefe9f3..de32927d816 100644 --- a/extensions/typescript-language-features/src/typescriptService.ts +++ b/extensions/typescript-language-features/src/typescriptService.ts @@ -4,12 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import BufferSyncSupport from './features/bufferSyncSupport'; import * as Proto from './protocol'; +import BufferSyncSupport from './tsServer/bufferSyncSupport'; +import { ExectuionTarget } from './tsServer/server'; +import { TypeScriptVersion } from './tsServer/versionProvider'; import API from './utils/api'; import { TypeScriptServiceConfiguration } from './utils/configuration'; import { PluginManager } from './utils/plugins'; -import { TypeScriptVersion } from './utils/versionProvider'; +import { TelemetryReporter } from './utils/telemetry'; export namespace ServerResponse { @@ -82,11 +84,24 @@ export type TypeScriptRequests = StandardTsServerRequests & NoResponseTsServerRe export type ExecConfig = { readonly lowPriority?: boolean; readonly nonRecoverable?: boolean; - readonly cancelOnResourceChange?: vscode.Uri + readonly cancelOnResourceChange?: vscode.Uri; + readonly executionTarget?: ExectuionTarget; }; export enum ClientCapability { + /** + * Basic syntax server. All clients should support this. + */ Syntax, + + /** + * Advanced syntax server that can provide single file IntelliSense. + */ + EnhancedSyntax, + + /** + * Complete, multi-file semantic server + */ Semantic, } @@ -138,16 +153,18 @@ export interface ITypeScriptServiceClient { readonly onTypesInstallerInitializationFailed: vscode.Event; readonly capabilities: ClientCapabilities; - readonly onDidChangeCapabilities: vscode.Event; + readonly onDidChangeCapabilities: vscode.Event; onReady(f: () => void): Promise; showVersionPicker(): void; readonly apiVersion: API; + readonly pluginManager: PluginManager; readonly configuration: TypeScriptServiceConfiguration; readonly bufferSyncSupport: BufferSyncSupport; + readonly telemetryReporter: TelemetryReporter; execute( command: K, diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 0e9dd740636..69a3c049040 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -3,31 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import BufferSyncSupport from './features/bufferSyncSupport'; -import { DiagnosticKind, DiagnosticsManager } from './features/diagnostics'; +import { DiagnosticKind, DiagnosticsManager } from './languageFeatures/diagnostics'; import * as Proto from './protocol'; import { EventName } from './protocol.const'; -import { ITypeScriptServer } from './tsServer/server'; +import BufferSyncSupport from './tsServer/bufferSyncSupport'; +import { OngoingRequestCancellerFactory } from './tsServer/cancellation'; +import { ILogDirectoryProvider } from './tsServer/logDirectoryProvider'; +import { ITypeScriptServer, TsServerProcessFactory } from './tsServer/server'; import { TypeScriptServerError } from './tsServer/serverError'; import { TypeScriptServerSpawner } from './tsServer/spawner'; +import { TypeScriptVersionManager } from './tsServer/versionManager'; +import { ITypeScriptVersionProvider, TypeScriptVersion } from './tsServer/versionProvider'; import { ClientCapabilities, ClientCapability, ExecConfig, ITypeScriptServiceClient, ServerResponse, TypeScriptRequests } from './typescriptService'; import API from './utils/api'; import { TsServerLogLevel, TypeScriptServiceConfiguration } from './utils/configuration'; import { Disposable } from './utils/dispose'; import * as fileSchemes from './utils/fileSchemes'; -import LogDirectoryProvider from './utils/logDirectoryProvider'; -import Logger from './utils/logger'; +import { Logger } from './utils/logger'; +import { isWeb } from './utils/platform'; import { TypeScriptPluginPathsProvider } from './utils/pluginPathsProvider'; import { PluginManager } from './utils/plugins'; import { TelemetryProperties, TelemetryReporter, VSCodeTelemetryReporter } from './utils/telemetry'; import Tracer from './utils/tracer'; import { inferredProjectCompilerOptions, ProjectType } from './utils/tsconfig'; -import { TypeScriptVersionManager } from './utils/versionManager'; -import { TypeScriptVersion, TypeScriptVersionProvider } from './utils/versionProvider'; const localize = nls.loadMessageBundle(); @@ -92,14 +93,12 @@ namespace ServerState { } export default class TypeScriptServiceClient extends Disposable implements ITypeScriptServiceClient { - private static readonly WALK_THROUGH_SNIPPET_SCHEME_COLON = `${fileSchemes.walkThroughSnippet}:`; private readonly pathSeparator: string; private readonly inMemoryResourcePrefix = '^'; private _onReady?: { promise: Promise; resolve: () => void; reject: () => void; }; private _configuration: TypeScriptServiceConfiguration; - private versionProvider: TypeScriptVersionProvider; private pluginPathsProvider: TypeScriptPluginPathsProvider; private readonly _versionManager: TypeScriptVersionManager; @@ -116,17 +115,35 @@ export default class TypeScriptServiceClient extends Disposable implements IType private readonly loadingIndicator = new ServerInitializingIndicator(); public readonly telemetryReporter: TelemetryReporter; - public readonly bufferSyncSupport: BufferSyncSupport; public readonly diagnosticsManager: DiagnosticsManager; + public readonly pluginManager: PluginManager; + + private readonly logDirectoryProvider: ILogDirectoryProvider; + private readonly cancellerFactory: OngoingRequestCancellerFactory; + private readonly versionProvider: ITypeScriptVersionProvider; + private readonly processFactory: TsServerProcessFactory; constructor( private readonly workspaceState: vscode.Memento, - public readonly pluginManager: PluginManager, - private readonly logDirectoryProvider: LogDirectoryProvider, + onCaseInsenitiveFileSystem: boolean, + services: { + pluginManager: PluginManager, + logDirectoryProvider: ILogDirectoryProvider, + cancellerFactory: OngoingRequestCancellerFactory, + versionProvider: ITypeScriptVersionProvider, + processFactory: TsServerProcessFactory, + }, allModeIds: readonly string[] ) { super(); + + this.pluginManager = services.pluginManager; + this.logDirectoryProvider = services.logDirectoryProvider; + this.cancellerFactory = services.cancellerFactory; + this.versionProvider = services.versionProvider; + this.processFactory = services.processFactory; + this.pathSeparator = path.sep; this.lastStart = Date.now(); @@ -141,17 +158,18 @@ export default class TypeScriptServiceClient extends Disposable implements IType this.numberRestarts = 0; this._configuration = TypeScriptServiceConfiguration.loadFromWorkspace(); - this.versionProvider = new TypeScriptVersionProvider(this._configuration); + this.versionProvider.updateConfiguration(this._configuration); + this.pluginPathsProvider = new TypeScriptPluginPathsProvider(this._configuration); this._versionManager = this._register(new TypeScriptVersionManager(this._configuration, this.versionProvider, this.workspaceState)); this._register(this._versionManager.onDidPickNewVersion(() => { this.restartTsServer(); })); - this.bufferSyncSupport = new BufferSyncSupport(this, allModeIds); + this.bufferSyncSupport = new BufferSyncSupport(this, allModeIds, onCaseInsenitiveFileSystem); this.onReady(() => { this.bufferSyncSupport.listen(); }); - this.diagnosticsManager = new DiagnosticsManager('typescript'); + this.diagnosticsManager = new DiagnosticsManager('typescript', onCaseInsenitiveFileSystem); this.bufferSyncSupport.onDelete(resource => { this.cancelInflightRequestsForResource(resource); this.diagnosticsManager.delete(resource); @@ -192,7 +210,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType return this.apiVersion.fullVersionString; })); - this.typescriptServerSpawner = new TypeScriptServerSpawner(this.versionProvider, this.logDirectoryProvider, this.pluginPathsProvider, this.logger, this.telemetryReporter, this.tracer); + this.typescriptServerSpawner = new TypeScriptServerSpawner(this.versionProvider, this._versionManager, this.logDirectoryProvider, this.pluginPathsProvider, this.logger, this.telemetryReporter, this.tracer, this.processFactory); this._register(this.pluginManager.onDidUpdateConfig(update => { this.configurePlugin(update.pluginId, update.config); @@ -204,13 +222,25 @@ export default class TypeScriptServiceClient extends Disposable implements IType } public get capabilities() { + if (isWeb()) { + return new ClientCapabilities( + ClientCapability.Syntax, + ClientCapability.EnhancedSyntax); + } + + if (this.apiVersion.gte(API.v400)) { + return new ClientCapabilities( + ClientCapability.Syntax, + ClientCapability.EnhancedSyntax, + ClientCapability.Semantic); + } + return new ClientCapabilities( - ClientCapability.Semantic, ClientCapability.Syntax, - ); + ClientCapability.Semantic); } - private readonly _onDidChangeCapabilities = this._register(new vscode.EventEmitter()); + private readonly _onDidChangeCapabilities = this._register(new vscode.EventEmitter()); readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event; private cancelInflightRequestsForResource(resource: vscode.Uri): void { @@ -336,18 +366,18 @@ export default class TypeScriptServiceClient extends Disposable implements IType } let version = this._versionManager.currentVersion; - - this.info(`Using tsserver from: ${version.path}`); - if (!fs.existsSync(version.tsServerPath)) { + if (!version.isValid) { vscode.window.showWarningMessage(localize('noServerFound', 'The path {0} doesn\'t point to a valid tsserver install. Falling back to bundled TypeScript version.', version.path)); this._versionManager.reset(); version = this._versionManager.currentVersion; } + this.info(`Using tsserver from: ${version.path}`); + const apiVersion = version.apiVersion || API.defaultVersion; let mytoken = ++this.token; - const handle = this.typescriptServerSpawner.spawn(version, this.capabilities, this.configuration, this.pluginManager, { + const handle = this.typescriptServerSpawner.spawn(version, this.capabilities, this.configuration, this.pluginManager, this.cancellerFactory, { onFatalError: (command, err) => this.fatalError(command, err), }); this.serverState = new ServerState.Running(handle, apiVersion, undefined, true); @@ -426,7 +456,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType handle.onEvent(event => this.dispatchEvent(event)); - if (apiVersion.gte(API.v300)) { + if (apiVersion.gte(API.v300) && this.capabilities.has(ClientCapability.Semantic)) { this.loadingIndicator.startedLoadingProject(undefined /* projectName */); } @@ -434,7 +464,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType this._onReady!.resolve(); this._onTsServerStarted.fire({ version: version, usedApiVersion: apiVersion }); - + this._onDidChangeCapabilities.fire(); return this.serverState; } @@ -502,6 +532,8 @@ 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 }; @@ -605,27 +637,27 @@ export default class TypeScriptServiceClient extends Disposable implements IType } public normalizedPath(resource: vscode.Uri): string | undefined { - if (resource.scheme === fileSchemes.walkThroughSnippet || resource.scheme === fileSchemes.untitled) { - const dirName = path.dirname(resource.path); - const fileName = this.inMemoryResourcePrefix + path.basename(resource.path); - return resource.with({ path: path.posix.join(dirName, fileName), query: '' }).toString(true); - } + switch (resource.scheme) { + case fileSchemes.file: + { + let result = resource.fsPath; + if (!result) { + return undefined; + } + result = path.normalize(result); - if (resource.scheme !== fileSchemes.file) { - return undefined; + // 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); + } } - - let result = resource.fsPath; - if (!result) { - return undefined; - } - - if (resource.scheme === fileSchemes.file) { - result = path.normalize(result); - } - - // Both \ and / must be escaped in regular expressions - return result.replace(new RegExp('\\' + this.pathSeparator, 'g'), '/'); } public toPath(resource: vscode.Uri): string | undefined { @@ -641,20 +673,10 @@ export default class TypeScriptServiceClient extends Disposable implements IType } public toResource(filepath: string): vscode.Uri { - if (filepath.match(/^[a-z]{2,}:/) || filepath.startsWith(TypeScriptServiceClient.WALK_THROUGH_SNIPPET_SCHEME_COLON) || (filepath.startsWith(fileSchemes.untitled + ':')) - ) { - let resource = vscode.Uri.parse(filepath); - const dirName = path.dirname(resource.path); - const fileName = path.basename(resource.path); - if (fileName.startsWith(this.inMemoryResourcePrefix)) { - resource = resource.with({ - path: path.posix.join(dirName, fileName.slice(this.inMemoryResourcePrefix.length)) - }); - } - + if (filepath.startsWith(this.inMemoryResourcePrefix)) { + const resource = vscode.Uri.parse(filepath.slice(1)); return this.bufferSyncSupport.toVsCodeResource(resource); } - return this.bufferSyncSupport.toResource(filepath); } @@ -732,9 +754,9 @@ export default class TypeScriptServiceClient extends Disposable implements IType }); } - private executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined; - private executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise>; - private executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise> | undefined { + private executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean, requireSemantic?: boolean }): undefined; + private executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, requireSemantic?: boolean }): Promise>; + private executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, requireSemantic?: boolean }): Promise> | undefined { this.bufferSyncSupport.beforeCommand(command); const runningServerState = this.service(); return runningServerState.server.executeImpl(command, args, executeInfo); @@ -930,7 +952,7 @@ The log file may contain personal data, including full paths and source code fro sections.push(`**TS Server Log** -Server logging disabled. To help us fix crashes like this, please enable logging by setting: +ā—ļøServer logging disabled. To help us fix crashes like this, please enable logging by setting: \`\`\`json "typescript.tsserver.log": "verbose" @@ -941,6 +963,8 @@ After enabling this setting, future crash reports will include the server log.`) sections.push(`**TS Server Error Stack** +Server: \`${error.serverId}\` + \`\`\` ${error.serverStack} \`\`\``); diff --git a/extensions/typescript-language-features/src/utils/api.ts b/extensions/typescript-language-features/src/utils/api.ts index 2a72b19004c..f797e578121 100644 --- a/extensions/typescript-language-features/src/utils/api.ts +++ b/extensions/typescript-language-features/src/utils/api.ts @@ -34,6 +34,7 @@ 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 fromVersionString(versionString: string): API { diff --git a/extensions/typescript-language-features/src/utils/configuration.ts b/extensions/typescript-language-features/src/utils/configuration.ts index adeccbb8efc..cde1e14b9a0 100644 --- a/extensions/typescript-language-features/src/utils/configuration.ts +++ b/extensions/typescript-language-features/src/utils/configuration.ts @@ -66,6 +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 static loadFromWorkspace(): TypeScriptServiceConfiguration { return new TypeScriptServiceConfiguration(); @@ -88,6 +89,7 @@ export class TypeScriptServiceConfiguration { this.maxTsServerMemory = TypeScriptServiceConfiguration.readMaxTsServerMemory(configuration); this.enablePromptUseWorkspaceTsdk = TypeScriptServiceConfiguration.readEnablePromptUseWorkspaceTsdk(configuration); this.watchOptions = TypeScriptServiceConfiguration.readWatchOptions(configuration); + this.includePackageJsonAutoImports = TypeScriptServiceConfiguration.readIncludePackageJsonAutoImports(configuration); } public isEqualTo(other: TypeScriptServiceConfiguration): boolean { @@ -104,7 +106,8 @@ export class TypeScriptServiceConfiguration { && this.enableProjectDiagnostics === other.enableProjectDiagnostics && this.maxTsServerMemory === other.maxTsServerMemory && objects.equals(this.watchOptions, other.watchOptions) - && this.enablePromptUseWorkspaceTsdk === other.enablePromptUseWorkspaceTsdk; + && this.enablePromptUseWorkspaceTsdk === other.enablePromptUseWorkspaceTsdk + && this.includePackageJsonAutoImports === other.includePackageJsonAutoImports; } private static fixPathPrefixes(inspectValue: string): string { @@ -178,6 +181,10 @@ 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 readMaxTsServerMemory(configuration: vscode.WorkspaceConfiguration): number { const defaultMaxMemory = 3072; const minimumMaxMemory = 128; diff --git a/extensions/typescript-language-features/src/utils/dependentRegistration.ts b/extensions/typescript-language-features/src/utils/dependentRegistration.ts index a10a33ba80c..29597ab50cb 100644 --- a/extensions/typescript-language-features/src/utils/dependentRegistration.ts +++ b/extensions/typescript-language-features/src/utils/dependentRegistration.ts @@ -96,12 +96,12 @@ export function requireConfiguration( ); } -export function requireCapability( +export function requireSomeCapability( client: ITypeScriptServiceClient, - requiredCapability: ClientCapability, + ...capabilities: readonly ClientCapability[] ) { return new Condition( - () => client.capabilities.has(requiredCapability), + () => capabilities.some(requiredCapability => client.capabilities.has(requiredCapability)), client.onDidChangeCapabilities ); } diff --git a/extensions/typescript-language-features/src/utils/documentSelector.ts b/extensions/typescript-language-features/src/utils/documentSelector.ts new file mode 100644 index 00000000000..0574c3f587d --- /dev/null +++ b/extensions/typescript-language-features/src/utils/documentSelector.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +export interface DocumentSelector { + /** + * Selector for files which only require a basic syntax server. + */ + readonly syntax: vscode.DocumentFilter[]; + + /** + * Selector for files which require semantic server support. + */ + readonly semantic: vscode.DocumentFilter[]; +} diff --git a/extensions/typescript-language-features/src/utils/electron.ts b/extensions/typescript-language-features/src/utils/electron.ts deleted file mode 100644 index ea16bae4277..00000000000 --- a/extensions/typescript-language-features/src/utils/electron.ts +++ /dev/null @@ -1,72 +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 temp from './temp'; -import path = require('path'); -import fs = require('fs'); -import cp = require('child_process'); -import process = require('process'); - - -const getRootTempDir = (() => { - let dir: string | undefined; - return () => { - if (!dir) { - dir = temp.getTempFile(`vscode-typescript${process.platform !== 'win32' && process.getuid ? process.getuid() : ''}`); - } - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); - } - return dir; - }; -})(); - -export const getInstanceDir = (() => { - let dir: string | undefined; - return () => { - if (!dir) { - dir = path.join(getRootTempDir(), temp.makeRandomHexString(20)); - } - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); - } - return dir; - }; -})(); - -export function getTempFile(prefix: string): string { - return path.join(getInstanceDir(), `${prefix}-${temp.makeRandomHexString(20)}.tmp`); -} - -function generatePatchedEnv(env: any, modulePath: string): any { - const newEnv = Object.assign({}, env); - - newEnv['ELECTRON_RUN_AS_NODE'] = '1'; - newEnv['NODE_PATH'] = path.join(modulePath, '..', '..', '..'); - - // Ensure we always have a PATH set - newEnv['PATH'] = newEnv['PATH'] || process.env.PATH; - - return newEnv; -} - -export interface ForkOptions { - readonly cwd?: string; - readonly execArgv?: string[]; -} - -export function fork( - modulePath: string, - args: string[], - options: ForkOptions, -): cp.ChildProcess { - const newEnv = generatePatchedEnv(process.env, modulePath); - return cp.fork(modulePath, args, { - silent: true, - cwd: options.cwd, - env: newEnv, - execArgv: options.execArgv - }); -} diff --git a/extensions/typescript-language-features/src/utils/fileSchemes.ts b/extensions/typescript-language-features/src/utils/fileSchemes.ts index 6efcfab4d3b..4e94d547bd6 100644 --- a/extensions/typescript-language-features/src/utils/fileSchemes.ts +++ b/extensions/typescript-language-features/src/utils/fileSchemes.ts @@ -8,12 +8,7 @@ export const untitled = 'untitled'; export const git = 'git'; export const walkThroughSnippet = 'walkThroughSnippet'; -export const supportedSchemes = [ +export const semanticSupportedSchemes = [ file, untitled, - walkThroughSnippet ]; - -export function isSupportedScheme(scheme: string): boolean { - return supportedSchemes.indexOf(scheme) >= 0; -} diff --git a/extensions/typescript-language-features/src/utils/fileSystem.electron.ts b/extensions/typescript-language-features/src/utils/fileSystem.electron.ts new file mode 100644 index 00000000000..3a6711224e2 --- /dev/null +++ b/extensions/typescript-language-features/src/utils/fileSystem.electron.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 * as fs from 'fs'; +import { getTempFile } from './temp.electron'; + +export const onCaseInsenitiveFileSystem = (() => { + let value: boolean | undefined; + return (): boolean => { + if (typeof value === 'undefined') { + if (process.platform === 'win32') { + value = true; + } else if (process.platform !== 'darwin') { + value = false; + } else { + const temp = getTempFile('typescript-case-check'); + fs.writeFileSync(temp, ''); + value = fs.existsSync(temp.toUpperCase()); + } + } + return value; + }; +})(); diff --git a/extensions/typescript-language-features/src/utils/largeProjectStatus.ts b/extensions/typescript-language-features/src/utils/largeProjectStatus.ts index f820101f1b1..223d7cb4716 100644 --- a/extensions/typescript-language-features/src/utils/largeProjectStatus.ts +++ b/extensions/typescript-language-features/src/utils/largeProjectStatus.ts @@ -111,16 +111,15 @@ function onConfigureExcludesSelected( export function create( client: ITypeScriptServiceClient, - telemetryReporter: TelemetryReporter -) { +): vscode.Disposable { const toDispose: vscode.Disposable[] = []; - const item = new ExcludeHintItem(telemetryReporter); + const item = new ExcludeHintItem(client.telemetryReporter); toDispose.push(vscode.commands.registerCommand('js.projectStatus.command', () => { if (item.configFileName) { onConfigureExcludesSelected(client, item.configFileName); } - let { message } = item.getCurrentHint(); + const { message } = item.getCurrentHint(); return vscode.window.showInformationMessage(message); })); @@ -128,4 +127,3 @@ export function create( return vscode.Disposable.from(...toDispose); } - diff --git a/extensions/typescript-language-features/src/utils/logger.ts b/extensions/typescript-language-features/src/utils/logger.ts index 18317e58e00..74b9fbcbf08 100644 --- a/extensions/typescript-language-features/src/utils/logger.ts +++ b/extensions/typescript-language-features/src/utils/logger.ts @@ -11,7 +11,7 @@ const localize = nls.loadMessageBundle(); type LogLevel = 'Trace' | 'Info' | 'Error'; -export default class Logger { +export class Logger { @memoize private get output(): vscode.OutputChannel { diff --git a/extensions/github-browser/extension.webpack.config.js b/extensions/typescript-language-features/src/utils/platform.ts similarity index 65% rename from extensions/github-browser/extension.webpack.config.js rename to extensions/typescript-language-features/src/utils/platform.ts index 45600607fc5..2d754bf4054 100644 --- a/extensions/github-browser/extension.webpack.config.js +++ b/extensions/typescript-language-features/src/utils/platform.ts @@ -3,15 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -//@ts-check +import * as vscode from 'vscode'; -'use strict'; - -const withDefaults = require('../shared.webpack.config'); - -module.exports = withDefaults({ - context: __dirname, - entry: { - extension: './src/extension.ts' - } -}); +export function isWeb(): boolean { + // @ts-expect-error + return typeof navigator !== 'undefined' && vscode.env.uiKind === vscode.UIKind.Web; +} diff --git a/extensions/typescript-language-features/src/utils/resourceMap.ts b/extensions/typescript-language-features/src/utils/resourceMap.ts index e942fae4583..ea4889ded35 100644 --- a/extensions/typescript-language-features/src/utils/resourceMap.ts +++ b/extensions/typescript-language-features/src/utils/resourceMap.ts @@ -3,10 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import * as vscode from 'vscode'; -import { memoize } from './memoize'; -import { getTempFile } from './temp'; /** * Maps of file resources @@ -18,7 +15,10 @@ export class ResourceMap { private readonly _map = new Map(); constructor( - private readonly _normalizePath: (resource: vscode.Uri) => string | undefined = (resource) => resource.fsPath + private readonly _normalizePath: (resource: vscode.Uri) => string | undefined = (resource) => resource.fsPath, + protected readonly config: { + readonly onCaseInsenitiveFileSystem: boolean, + }, ) { } public get size() { @@ -83,23 +83,10 @@ export class ResourceMap { if (isWindowsPath(path)) { return true; } - return path[0] === '/' && this.onIsCaseInsenitiveFileSystem; - } - - @memoize - private get onIsCaseInsenitiveFileSystem() { - if (process.platform === 'win32') { - return true; - } - if (process.platform !== 'darwin') { - return false; - } - const temp = getTempFile('typescript-case-check'); - fs.writeFileSync(temp, ''); - return fs.existsSync(temp.toUpperCase()); + return path[0] === '/' && this.config.onCaseInsenitiveFileSystem; } } -export function isWindowsPath(path: string): boolean { +function isWindowsPath(path: string): boolean { return /^[a-zA-Z]:[\/\\]/.test(path); } diff --git a/extensions/typescript-language-features/src/utils/temp.electron.ts b/extensions/typescript-language-features/src/utils/temp.electron.ts new file mode 100644 index 00000000000..cd79b36415d --- /dev/null +++ b/extensions/typescript-language-features/src/utils/temp.electron.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as os from 'os'; +import * as fs from 'fs'; +import * as path from 'path'; + +function makeRandomHexString(length: number): string { + const chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; + let result = ''; + for (let i = 0; i < length; i++) { + const idx = Math.floor(chars.length * Math.random()); + result += chars[idx]; + } + return result; +} + +const getRootTempDir = (() => { + let dir: string | undefined; + return () => { + if (!dir) { + const filename = `vscode-typescript${process.platform !== 'win32' && process.getuid ? process.getuid() : ''}`; + dir = path.join(os.tmpdir(), filename); + } + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + return dir; + }; +})(); + +export const getInstanceTempDir = (() => { + let dir: string | undefined; + return () => { + if (!dir) { + dir = path.join(getRootTempDir(), makeRandomHexString(20)); + } + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + return dir; + }; +})(); + +export function getTempFile(prefix: string): string { + return path.join(getInstanceTempDir(), `${prefix}-${makeRandomHexString(20)}.tmp`); +} diff --git a/extensions/typescript-language-features/src/utils/temp.ts b/extensions/typescript-language-features/src/utils/temp.ts deleted file mode 100644 index 2af5f1732b0..00000000000 --- a/extensions/typescript-language-features/src/utils/temp.ts +++ /dev/null @@ -1,21 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import path = require('path'); -import os = require('os'); - -export function makeRandomHexString(length: number): string { - const chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; - let result = ''; - for (let i = 0; i < length; i++) { - const idx = Math.floor(chars.length * Math.random()); - result += chars[idx]; - } - return result; -} - -export function getTempFile(name: string): string { - return path.join(os.tmpdir(), name); -} \ No newline at end of file diff --git a/extensions/typescript-language-features/src/utils/tracer.ts b/extensions/typescript-language-features/src/utils/tracer.ts index 134ae13d5de..445b828d0c6 100644 --- a/extensions/typescript-language-features/src/utils/tracer.ts +++ b/extensions/typescript-language-features/src/utils/tracer.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import type * as Proto from '../protocol'; -import Logger from './logger'; +import { Logger } from './logger'; enum Trace { Off, diff --git a/extensions/typescript-language-features/yarn.lock b/extensions/typescript-language-features/yarn.lock index 14df7e9f12c..147be55e1c8 100644 --- a/extensions/typescript-language-features/yarn.lock +++ b/extensions/typescript-language-features/yarn.lock @@ -2,6 +2,34 @@ # yarn lockfile v1 +"@nodelib/fs.scandir@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" + integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== + dependencies: + "@nodelib/fs.stat" "2.0.3" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" + integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" + integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== + dependencies: + "@nodelib/fs.scandir" "2.1.3" + fastq "^1.6.0" + +"@npmcli/move-file@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.0.1.tgz#de103070dac0f48ce49cf6693c23af59c0f70464" + integrity sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw== + dependencies: + mkdirp "^1.0.4" + "@types/events@*": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" @@ -16,6 +44,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/json-schema@^7.0.4": + version "7.0.5" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" + integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -51,6 +84,29 @@ agent-base@4, agent-base@^4.3.0: dependencies: es6-promisify "^5.0.0" +aggregate-error@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0" + integrity sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv-keywords@^3.4.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.1.tgz#b83ca89c5d42d69031f424cad49aada0236c6957" + integrity sha512-KWcq3xN8fDjSB+IMoh2VaXVhRI0BBGxoYp3rx7Pkb6z0cFjYR9Q9l4yZqqals0/zsioCmocC5H6UvsGD4MoIBA== + +ajv@^6.12.2: + version "6.12.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" + integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + ajv@^6.5.5: version "6.10.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" @@ -70,6 +126,11 @@ applicationinsights@1.0.8: diagnostic-channel-publishers "0.2.1" zone.js "0.7.6" +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" @@ -109,6 +170,11 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -117,6 +183,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +braces@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + browser-stdout@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" @@ -127,11 +200,44 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +cacache@^15.0.4: + version "15.0.5" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.5.tgz#69162833da29170d6732334643c60e005f5f17d0" + integrity sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A== + dependencies: + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.0" + tar "^6.0.2" + unique-filename "^1.1.1" + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -144,11 +250,38 @@ commander@2.15.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +copy-webpack-plugin@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-6.0.3.tgz#2b3d2bfc6861b96432a65f0149720adbd902040b" + integrity sha512-q5m6Vz4elsuyVEIUXr7wJdIdePWTubsqVbEMvf1WQnHGv0Q+9yPRu7MtYFPt+GBOXRav9lvIINifTQ1vSCs+eA== + dependencies: + cacache "^15.0.4" + fast-glob "^3.2.4" + find-cache-dir "^3.3.1" + glob-parent "^5.1.1" + globby "^11.0.1" + loader-utils "^2.0.0" + normalize-path "^3.0.0" + p-limit "^3.0.1" + schema-utils "^2.7.0" + serialize-javascript "^4.0.0" + webpack-sources "^1.4.3" + core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -197,6 +330,13 @@ diff@3.5.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -205,6 +345,11 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + es6-promise@^4.0.3: version "4.2.8" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" @@ -242,11 +387,59 @@ fast-deep-equal@^2.0.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.1.1, fast-glob@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" + integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.0" + merge2 "^1.3.0" + micromatch "^4.0.2" + picomatch "^2.2.1" + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity "sha1-h0v2nG9ATCtdmcSBNBOZ/VWJJjM= sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" +fastq@^1.6.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" + integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q== + dependencies: + reusify "^1.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-cache-dir@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" + integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -261,6 +454,13 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -273,6 +473,13 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +glob-parent@^5.1.0, glob-parent@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + glob@7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" @@ -285,7 +492,7 @@ glob@7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.2, glob@^7.1.3: +glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -297,6 +504,18 @@ glob@^7.1.2, glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +globby@^11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" + integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + growl@1.10.5: version "1.10.5" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" @@ -350,6 +569,26 @@ https-proxy-agent@^2.2.1: agent-base "^4.3.0" debug "^3.1.0" +ignore@^5.1.4: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -363,6 +602,23 @@ inherits@2: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -393,6 +649,13 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= +json5@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" + integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== + dependencies: + minimist "^1.2.5" + jsonc-parser@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.2.1.tgz#db73cd59d78cce28723199466b2a03d1be1df2bc" @@ -408,6 +671,49 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +loader-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" + integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-dir@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + mime-db@1.43.0: version "1.43.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" @@ -432,6 +738,47 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= +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== + +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.3.tgz#55f7839307d74859d6e8ada9c3ebe72cec216a34" + integrity sha512-cFOknTvng5vqnwOpDsZTWhNll6Jf8o2x+/diplafmxpuIymAjzoOolZG0VvQf3V2HgqzJNhnuKHYp2BqDgz8IQ== + dependencies: + minipass "^3.0.0" + +minipass@^3.0.0, minipass@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" + integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + dependencies: + yallist "^4.0.0" + +minizlib@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.0.tgz#fd52c645301ef09a63a2c209697c294c6ce02cf3" + integrity sha512-EzTZN/fjSvifSX0SlqUERCN39o6T40AMarPbv0MrarSFtIITCBh7bi+dU8nxGFHuqs9jdIAeoYoKuQAAASsPPA== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + mkdirp@0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -439,6 +786,11 @@ mkdirp@0.5.1: dependencies: minimist "0.0.8" +mkdirp@^1.0.3, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + mocha@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" @@ -466,6 +818,11 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" @@ -478,16 +835,76 @@ once@^1.3.0: dependencies: wrappy "1" +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe" + integrity sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg== + dependencies: + p-try "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= +picomatch@^2.0.5, picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + +pkg-dir@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= + psl@^1.1.24: version "1.7.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c" @@ -513,6 +930,13 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + request@^2.88.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" @@ -544,6 +968,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -551,16 +980,42 @@ rimraf@^2.6.3: dependencies: glob "^7.1.3" +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" + integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== + safe-buffer@^5.0.1, safe-buffer@^5.1.2: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== +safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +schema-utils@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" + integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== + dependencies: + "@types/json-schema" "^7.0.4" + ajv "^6.12.2" + ajv-keywords "^3.4.1" + semver@5.5.1: version "5.5.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" @@ -571,6 +1026,28 @@ semver@^5.3.0, semver@^5.4.1: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +semver@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +source-list-map@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" + integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== + source-map-support@^0.5.0: version "0.5.16" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" @@ -579,7 +1056,15 @@ source-map-support@^0.5.0: buffer-from "^1.0.0" source-map "^0.6.0" -source-map@^0.6.0: +source-map-support@~0.5.12: + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -599,6 +1084,13 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +ssri@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.0.tgz#79ca74e21f8ceaeddfcb4b90143c458b8d988808" + integrity sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA== + dependencies: + minipass "^3.1.1" + supports-color@5.4.0: version "5.4.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" @@ -606,6 +1098,34 @@ supports-color@5.4.0: dependencies: has-flag "^3.0.0" +tar@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.2.tgz#5df17813468a6264ff14f766886c622b84ae2f39" + integrity sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.0" + mkdirp "^1.0.3" + yallist "^4.0.0" + +terser@^4.8.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" + integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== + dependencies: + commander "^2.20.0" + source-map "~0.6.1" + source-map-support "~0.5.12" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + tough-cookie@~2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" @@ -631,6 +1151,24 @@ typescript-vscode-sh-plugin@^0.6.14: resolved "https://registry.yarnpkg.com/typescript-vscode-sh-plugin/-/typescript-vscode-sh-plugin-0.6.14.tgz#a81031b502f6346a26ea49ce082438c3e353bb38" integrity sha512-AkNlRBbI6K7gk29O92qthNSvc6jjmNQ6isVXoYxkFwPa8D04tIv2SOPd+sd+mNpso4tNdL2gy7nVtrd5yFqvlA== +"typescript-web-server@git://github.com/mjbvz/ts-server-web-build": + version "0.0.0" + resolved "git://github.com/mjbvz/ts-server-web-build#1d85be25043f9b5e36a531941ea345dd5a2ca007" + +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" @@ -693,11 +1231,24 @@ vscode@^1.1.36: url-parse "^1.4.4" vscode-test "^0.4.1" +webpack-sources@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + zone.js@0.7.6: version "0.7.6" resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009" diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts index 6177c155b52..fd97ea91728 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts @@ -17,7 +17,7 @@ function workspaceFile(...segments: string[]) { const testDocument = workspaceFile('bower.json'); -suite('vscode API - webview', () => { +suite.skip('vscode API - webview', () => { const disposables: vscode.Disposable[] = []; function _register(disposable: T) { @@ -86,7 +86,7 @@ suite('vscode API - webview', () => { } }); - test('webviews should preserve vscode API state when they are hidden', async () => { + test.skip('webviews should preserve vscode API state when they are hidden', async () => { const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true })); const ready = getMesssage(webview); webview.webview.html = createHtmlDocumentWithBody(/*html*/` @@ -239,7 +239,7 @@ suite('vscode API - webview', () => { }); - test('webviews should only be able to load resources from workspace by default', async () => { + test.skip('webviews should only be able to load resources from workspace by default', async () => { const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { @@ -272,6 +272,18 @@ suite('vscode API - webview', () => { const response = await sendRecieveMessage(webview, { src: imagePath.toString() }); assert.strictEqual(response.value, true); } + // { + // // #102188. Resource filename containing special characters like '%', '#', '?'. + // const imagePath = webview.webview.asWebviewUri(workspaceFile('image%02.png')); + // const response = await sendRecieveMessage(webview, { src: imagePath.toString() }); + // assert.strictEqual(response.value, true); + // } + // { + // // #102188. Resource filename containing special characters like '%', '#', '?'. + // const imagePath = webview.webview.asWebviewUri(workspaceFile('image%.png')); + // const response = await sendRecieveMessage(webview, { src: imagePath.toString() }); + // assert.strictEqual(response.value, true); + // } { const imagePath = webview.webview.asWebviewUri(workspaceFile('no-such-image.png')); const response = await sendRecieveMessage(webview, { src: imagePath.toString() }); @@ -284,7 +296,7 @@ suite('vscode API - webview', () => { } }); - test('webviews should allow overriding allowed resource paths using localResourceRoots', async () => { + test.skip('webviews should allow overriding allowed resource paths using localResourceRoots', async () => { const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true, localResourceRoots: [workspaceFile('sub')] @@ -312,7 +324,7 @@ suite('vscode API - webview', () => { } }); - test('webviews using hard-coded old style vscode-resource uri should work', async () => { + test.skip('webviews using hard-coded old style vscode-resource uri should work', async () => { const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true, localResourceRoots: [workspaceFile('sub')] 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 4ec107c23be..53acdcf23dc 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 @@ -167,57 +167,47 @@ import { window, tasks, Disposable, TaskDefinition, Task, EventEmitter, CustomEx commands.executeCommand('workbench.action.tasks.runTask', `${taskType}: ${taskName}`); }); - test('Execution from onDidEndTaskProcess is equal to original', () => { - return new Promise(async (resolve, reject) => { + test('Execution from onDidEndTaskProcess and onDidStartTaskProcess are equal to original', () => { + return new Promise(async (resolve) => { const task = new Task({ type: 'testTask' }, TaskScope.Workspace, 'echo', 'testTask', new ShellExecution('echo', ['hello test'])); let taskExecution: TaskExecution | undefined; - - disposables.push(tasks.onDidStartTaskProcess(e => { - if (taskExecution === undefined) { - reject('taskExecution is still undefined when process started.'); - } else if (e.execution !== taskExecution) { - reject('Unexpected task execution value in start process.'); + const executeDoneEvent: EventEmitter = new EventEmitter(); + const taskExecutionShouldBeSet: Promise = new Promise(resolve => { + const disposable = executeDoneEvent.event(() => { + resolve(); + disposable.dispose(); + }); + }); + let count = 2; + const progressMade: EventEmitter = new EventEmitter(); + let startSucceeded = false; + let endSucceeded = false; + disposables.push(progressMade.event(() => { + count--; + if ((count === 0) && startSucceeded && endSucceeded) { + resolve(); } })); - disposables.push(tasks.onDidEndTaskProcess(e => { - if (taskExecution === undefined) { - reject('taskExecution is still undefined when process ended.'); - } else if (e.execution === taskExecution) { - resolve(); - } else { - reject('Unexpected task execution value in end process.'); - } - })); - - taskExecution = await tasks.executeTask(task); - }); - }); - - test('Execution from onDidStartTaskProcess is equal to original', () => { - return new Promise(async (resolve, reject) => { - const task = new Task({ type: 'testTask' }, TaskScope.Workspace, 'echo', 'testTask', new ShellExecution('echo', ['hello test'])); - let taskExecution: TaskExecution | undefined; - - disposables.push(tasks.onDidStartTaskProcess(e => { - if (taskExecution === undefined) { - reject('taskExecution is still undefined when process started.'); - } else if (e.execution === taskExecution) { - resolve(); - } else { - reject('Unexpected task execution value in start process.'); - } - })); - - disposables.push(tasks.onDidEndTaskProcess(e => { - if (taskExecution === undefined) { - reject('taskExecution is still undefined when process ended.'); - } else if (e.execution !== taskExecution) { - reject('Unexpected task execution value in end process.'); + + disposables.push(tasks.onDidStartTaskProcess(async (e) => { + await taskExecutionShouldBeSet; + if (e.execution === taskExecution) { + startSucceeded = true; + progressMade.fire(); + } + })); + + disposables.push(tasks.onDidEndTaskProcess(async (e) => { + await taskExecutionShouldBeSet; + if (e.execution === taskExecution) { + endSucceeded = true; + progressMade.fire(); } })); taskExecution = await tasks.executeTask(task); + executeDoneEvent.fire(); }); }); @@ -228,8 +218,10 @@ import { window, tasks, Disposable, TaskDefinition, Task, EventEmitter, CustomEx private readonly writeEmitter = new EventEmitter(); public readonly onDidWrite: Event = this.writeEmitter.event; public async close(): Promise { } + private closeEmitter = new EventEmitter(); + onDidClose: Event = this.closeEmitter.event; public open(): void { - this.close(); + this.closeEmitter.fire(); resolve(); } } diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index 3949c1d1727..6e1034001f4 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -551,7 +551,7 @@ suite('vscode API - workspace', () => { }); test('findFiles', () => { - return vscode.workspace.findFiles('**/*.png').then((res) => { + return vscode.workspace.findFiles('**/image.png').then((res) => { assert.equal(res.length, 2); assert.equal(basename(vscode.workspace.asRelativePath(res[0])), 'image.png'); }); @@ -572,14 +572,14 @@ suite('vscode API - workspace', () => { }); test('findFiles - exclude', () => { - return vscode.workspace.findFiles('**/*.png').then((res) => { + return vscode.workspace.findFiles('**/image.png').then((res) => { assert.equal(res.length, 2); assert.equal(basename(vscode.workspace.asRelativePath(res[0])), 'image.png'); }); }); test('findFiles, exclude', () => { - return vscode.workspace.findFiles('**/*.png', '**/sub/**').then((res) => { + return vscode.workspace.findFiles('**/image.png', '**/sub/**').then((res) => { assert.equal(res.length, 1); assert.equal(basename(vscode.workspace.asRelativePath(res[0])), 'image.png'); }); diff --git a/extensions/vscode-api-tests/testWorkspace/image%.png b/extensions/vscode-api-tests/testWorkspace/image%.png new file mode 100644 index 00000000000..15b462975be Binary files /dev/null and b/extensions/vscode-api-tests/testWorkspace/image%.png differ diff --git a/extensions/vscode-api-tests/testWorkspace/image%02.png b/extensions/vscode-api-tests/testWorkspace/image%02.png new file mode 100644 index 00000000000..15b462975be Binary files /dev/null and b/extensions/vscode-api-tests/testWorkspace/image%02.png differ diff --git a/extensions/vscode-custom-editor-tests/customEditorMedia/textEditor.js b/extensions/vscode-custom-editor-tests/customEditorMedia/textEditor.js new file mode 100644 index 00000000000..8978b201c9f --- /dev/null +++ b/extensions/vscode-custom-editor-tests/customEditorMedia/textEditor.js @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// @ts-check +(function () { + // @ts-ignore + const vscode = acquireVsCodeApi(); + + const textArea = document.querySelector('textarea'); + + const initialState = vscode.getState(); + if (initialState) { + textArea.value = initialState.value; + } + + window.addEventListener('message', e => { + switch (e.data.type) { + case 'fakeInput': + { + const value = e.data.value; + textArea.value = value; + onInput(); + break; + } + + case 'setValue': + { + const value = e.data.value; + textArea.value = value; + vscode.setState({ value }); + + vscode.postMessage({ + type: 'didChangeContent', + value: value + }); + break; + } + } + }); + + const onInput = () => { + const value = textArea.value; + vscode.setState({ value }); + vscode.postMessage({ + type: 'edit', + value: value + }); + vscode.postMessage({ + type: 'didChangeContent', + value: value + }); + }; + + textArea.addEventListener('input', onInput); +}()); diff --git a/extensions/vscode-custom-editor-tests/package.json b/extensions/vscode-custom-editor-tests/package.json new file mode 100644 index 00000000000..08b6702b80a --- /dev/null +++ b/extensions/vscode-custom-editor-tests/package.json @@ -0,0 +1,44 @@ +{ + "name": "vscode-custom-editor-tests", + "description": "Custom editor tests for VS Code", + "version": "0.0.1", + "publisher": "vscode", + "license": "MIT", + "private": true, + "activationEvents": [ + "onCustomEditor:testWebviewEditor.abc" + ], + "main": "./out/extension", + "enableProposedApi": true, + "engines": { + "vscode": "^1.48.0" + }, + "scripts": { + "compile": "node ./node_modules/vscode/bin/compile -watch -p ./", + "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-notebook-tests ./tsconfig.json" + }, + "dependencies": { + "p-limit": "^3.0.2" + }, + "devDependencies": { + "@types/node": "^12.11.7", + "@types/p-limit": "^2.2.0", + "mocha": "^2.3.3", + "mocha-junit-reporter": "^1.17.0", + "mocha-multi-reporters": "^1.1.7", + "vscode": "^1.1.36" + }, + "contributes": { + "customEditors": [ + { + "viewType": "testWebviewEditor.abc", + "displayName": "Test ABC editor", + "selector": [ + { + "filenamePattern": "*.abc" + } + ] + } + ] + } +} diff --git a/extensions/vscode-custom-editor-tests/src/customTextEditor.ts b/extensions/vscode-custom-editor-tests/src/customTextEditor.ts new file mode 100644 index 00000000000..cced14b9401 --- /dev/null +++ b/extensions/vscode-custom-editor-tests/src/customTextEditor.ts @@ -0,0 +1,165 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as pLimit from 'p-limit'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { Disposable } from './dispose'; + +export namespace Testing { + export const abcEditorContentChangeCommand = '_abcEditor.contentChange'; + export const abcEditorTypeCommand = '_abcEditor.type'; + + export interface CustomEditorContentChangeEvent { + readonly content: string; + readonly source: vscode.Uri; + } +} + +export class AbcTextEditorProvider implements vscode.CustomTextEditorProvider { + + public static readonly viewType = 'testWebviewEditor.abc'; + + private activeEditor?: AbcEditor; + + public constructor( + private readonly context: vscode.ExtensionContext, + ) { } + + public register(): vscode.Disposable { + const provider = vscode.window.registerCustomEditorProvider(AbcTextEditorProvider.viewType, this); + + const commands: vscode.Disposable[] = []; + commands.push(vscode.commands.registerCommand(Testing.abcEditorTypeCommand, (content: string) => { + this.activeEditor?.testing_fakeInput(content); + })); + + return vscode.Disposable.from(provider, ...commands); + } + + public async resolveCustomTextEditor(document: vscode.TextDocument, panel: vscode.WebviewPanel) { + const editor = new AbcEditor(document, this.context.extensionPath, panel); + + this.activeEditor = editor; + + panel.onDidChangeViewState(({ webviewPanel }) => { + if (this.activeEditor === editor && !webviewPanel.active) { + this.activeEditor = undefined; + } + if (webviewPanel.active) { + this.activeEditor = editor; + } + }); + } +} + +class AbcEditor extends Disposable { + + public readonly _onDispose = this._register(new vscode.EventEmitter()); + public readonly onDispose = this._onDispose.event; + + private readonly limit = pLimit(1); + private syncedVersion: number = -1; + private currentWorkspaceEdit?: Thenable; + + constructor( + private readonly document: vscode.TextDocument, + private readonly _extensionPath: string, + private readonly panel: vscode.WebviewPanel, + ) { + super(); + + panel.webview.options = { + enableScripts: true, + }; + panel.webview.html = this.html; + + this._register(vscode.workspace.onDidChangeTextDocument(e => { + if (e.document === this.document) { + this.update(); + } + })); + + this._register(panel.webview.onDidReceiveMessage(message => { + switch (message.type) { + case 'edit': + this.doEdit(message.value); + break; + + case 'didChangeContent': + vscode.commands.executeCommand(Testing.abcEditorContentChangeCommand, { + content: message.value, + source: document.uri, + } as Testing.CustomEditorContentChangeEvent); + break; + } + })); + + this._register(panel.onDidDispose(() => { this.dispose(); })); + + this.update(); + } + + public testing_fakeInput(value: string) { + this.panel.webview.postMessage({ + type: 'fakeInput', + value: value, + }); + } + + private async doEdit(value: string) { + const edit = new vscode.WorkspaceEdit(); + edit.replace(this.document.uri, this.document.validateRange(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(999999, 999999))), value); + this.limit(() => { + this.currentWorkspaceEdit = vscode.workspace.applyEdit(edit).then(() => { + this.syncedVersion = this.document.version; + this.currentWorkspaceEdit = undefined; + }); + return this.currentWorkspaceEdit; + }); + } + + public dispose() { + if (this.isDisposed) { + return; + } + + this._onDispose.fire(); + super.dispose(); + } + + private get html() { + const contentRoot = path.join(this._extensionPath, 'customEditorMedia'); + const scriptUri = vscode.Uri.file(path.join(contentRoot, 'textEditor.js')); + const nonce = Date.now() + ''; + return /* html */` + + + + + + Document + + + + + + `; + } + + public async update() { + await this.currentWorkspaceEdit; + + if (this.isDisposed || this.syncedVersion >= this.document.version) { + return; + } + + this.panel.webview.postMessage({ + type: 'setValue', + value: this.document.getText(), + }); + this.syncedVersion = this.document.version; + } +} diff --git a/extensions/vscode-custom-editor-tests/src/dispose.ts b/extensions/vscode-custom-editor-tests/src/dispose.ts new file mode 100644 index 00000000000..548094c28e5 --- /dev/null +++ b/extensions/vscode-custom-editor-tests/src/dispose.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +export function disposeAll(disposables: vscode.Disposable[]) { + while (disposables.length) { + const item = disposables.pop(); + if (item) { + item.dispose(); + } + } +} + +export abstract class Disposable { + private _isDisposed = false; + + protected _disposables: vscode.Disposable[] = []; + + public dispose(): any { + if (this._isDisposed) { + return; + } + this._isDisposed = true; + disposeAll(this._disposables); + } + + protected _register(value: T): T { + if (this._isDisposed) { + value.dispose(); + } else { + this._disposables.push(value); + } + return value; + } + + protected get isDisposed() { + return this._isDisposed; + } +} \ No newline at end of file diff --git a/extensions/vscode-custom-editor-tests/src/extension.ts b/extensions/vscode-custom-editor-tests/src/extension.ts new file mode 100644 index 00000000000..0a83f97fce2 --- /dev/null +++ b/extensions/vscode-custom-editor-tests/src/extension.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 * as vscode from 'vscode'; +import { AbcTextEditorProvider } from './customTextEditor'; + +export function activate(context: vscode.ExtensionContext) { + context.subscriptions.push(new AbcTextEditorProvider(context).register()); +} diff --git a/extensions/vscode-custom-editor-tests/src/test/customEditor.test.ts b/extensions/vscode-custom-editor-tests/src/test/customEditor.test.ts new file mode 100644 index 00000000000..d66ab665b8e --- /dev/null +++ b/extensions/vscode-custom-editor-tests/src/test/customEditor.test.ts @@ -0,0 +1,314 @@ +/*--------------------------------------------------------------------------------------------- + * 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 fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { Testing } from '../customTextEditor'; +import { closeAllEditors, delay, disposeAll, randomFilePath } from './utils'; + +assert.ok(vscode.workspace.rootPath); +const testWorkspaceRoot = vscode.Uri.file(path.join(vscode.workspace.rootPath!, 'customEditors')); + +const commands = Object.freeze({ + open: 'vscode.open', + openWith: 'vscode.openWith', + save: 'workbench.action.files.save', + undo: 'undo', +}); + +async function writeRandomFile(options: { ext: string; contents: string; }): Promise { + const fakeFile = randomFilePath({ root: testWorkspaceRoot, ext: options.ext }); + await fs.promises.writeFile(fakeFile.fsPath, Buffer.from(options.contents)); + return fakeFile; +} + +const disposables: vscode.Disposable[] = []; +function _register(disposable: T) { + disposables.push(disposable); + return disposable; +} + +class CustomEditorUpdateListener { + + public static create() { + return _register(new CustomEditorUpdateListener()); + } + + private readonly commandSubscription: vscode.Disposable; + + private readonly unconsumedResponses: Array = []; + private readonly callbackQueue: Array<(data: Testing.CustomEditorContentChangeEvent) => void> = []; + + private constructor() { + this.commandSubscription = vscode.commands.registerCommand(Testing.abcEditorContentChangeCommand, (data: Testing.CustomEditorContentChangeEvent) => { + if (this.callbackQueue.length) { + const callback = this.callbackQueue.shift(); + assert.ok(callback); + callback!(data); + } else { + this.unconsumedResponses.push(data); + } + }); + } + + dispose() { + this.commandSubscription.dispose(); + } + + async nextResponse(): Promise { + if (this.unconsumedResponses.length) { + return this.unconsumedResponses.shift()!; + } + + return new Promise(resolve => { + this.callbackQueue.push(resolve); + }); + } +} + + +suite('CustomEditor tests', () => { + setup(async () => { + await closeAllEditors(); + await resetTestWorkspace(); + }); + + teardown(async () => { + await closeAllEditors(); + disposeAll(disposables); + await resetTestWorkspace(); + }); + + test('Should load basic content from disk', async () => { + const startingContent = `load, init`; + const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent }); + + const listener = CustomEditorUpdateListener.create(); + + await vscode.commands.executeCommand(commands.open, testDocument); + + const { content } = await listener.nextResponse(); + assert.equal(content, startingContent); + }); + + test('Should support basic edits', async () => { + const startingContent = `basic edit, init`; + const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent }); + + const listener = CustomEditorUpdateListener.create(); + + await vscode.commands.executeCommand(commands.open, testDocument); + await listener.nextResponse(); + + const newContent = `basic edit test`; + await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, newContent); + const { content } = await listener.nextResponse(); + assert.equal(content, newContent); + }); + + test('Should support single undo', async () => { + const startingContent = `single undo, init`; + const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent }); + + const listener = CustomEditorUpdateListener.create(); + + await vscode.commands.executeCommand(commands.open, testDocument); + await listener.nextResponse(); + + const newContent = `undo test`; + { + await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, newContent); + const { content } = await listener.nextResponse(); + assert.equal(content, newContent); + } + await delay(100); + { + await vscode.commands.executeCommand(commands.undo); + const { content } = await listener.nextResponse(); + assert.equal(content, startingContent); + } + }); + + test('Should support multiple undo', async () => { + const startingContent = `multiple undo, init`; + const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent }); + + const listener = CustomEditorUpdateListener.create(); + + await vscode.commands.executeCommand(commands.open, testDocument); + await listener.nextResponse(); + + const count = 10; + + // Make edits + for (let i = 0; i < count; ++i) { + await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, `${i}`); + const { content } = await listener.nextResponse(); + assert.equal(`${i}`, content); + } + + // Then undo them in order + for (let i = count - 1; i; --i) { + await delay(100); + await vscode.commands.executeCommand(commands.undo); + const { content } = await listener.nextResponse(); + assert.equal(`${i - 1}`, content); + } + + { + await delay(100); + await vscode.commands.executeCommand(commands.undo); + const { content } = await listener.nextResponse(); + assert.equal(content, startingContent); + } + }); + + test('Should update custom editor on file move', async () => { + const startingContent = `file move, init`; + const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent }); + + const listener = CustomEditorUpdateListener.create(); + + await vscode.commands.executeCommand(commands.open, testDocument); + await listener.nextResponse(); + + const newFileName = vscode.Uri.file(path.join(testWorkspaceRoot.fsPath, 'y.abc')); + + const edit = new vscode.WorkspaceEdit(); + edit.renameFile(testDocument, newFileName); + + await vscode.workspace.applyEdit(edit); + + const response = (await listener.nextResponse()); + assert.equal(response.content, startingContent); + assert.equal(response.source.toString(), newFileName.toString()); + }); + + test('Should support saving custom editors', async () => { + const startingContent = `save, init`; + const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent }); + + const listener = CustomEditorUpdateListener.create(); + + await vscode.commands.executeCommand(commands.open, testDocument); + await listener.nextResponse(); + + const newContent = `save, new`; + { + await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, newContent); + const { content } = await listener.nextResponse(); + assert.equal(content, newContent); + } + { + await vscode.commands.executeCommand(commands.save); + const fileContent = (await fs.promises.readFile(testDocument.fsPath)).toString(); + assert.equal(fileContent, newContent); + } + }); + + test('Should undo after saving custom editor', async () => { + const startingContent = `undo after save, init`; + const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent }); + + const listener = CustomEditorUpdateListener.create(); + + await vscode.commands.executeCommand(commands.open, testDocument); + await listener.nextResponse(); + + const newContent = `undo after save, new`; + { + await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, newContent); + const { content } = await listener.nextResponse(); + assert.equal(content, newContent); + } + { + await vscode.commands.executeCommand(commands.save); + const fileContent = (await fs.promises.readFile(testDocument.fsPath)).toString(); + assert.equal(fileContent, newContent); + } + await delay(100); + { + await vscode.commands.executeCommand(commands.undo); + const { content } = await listener.nextResponse(); + assert.equal(content, startingContent); + } + }); + + test.skip('Should support untitled custom editors', async () => { + const listener = CustomEditorUpdateListener.create(); + + const untitledFile = randomFilePath({ root: testWorkspaceRoot, ext: '.abc' }).with({ scheme: 'untitled' }); + + await vscode.commands.executeCommand(commands.open, untitledFile); + assert.equal((await listener.nextResponse()).content, ''); + + await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, `123`); + assert.equal((await listener.nextResponse()).content, '123'); + + await vscode.commands.executeCommand(commands.save); + const content = await fs.promises.readFile(untitledFile.fsPath); + assert.equal(content.toString(), '123'); + }); + + test.skip('When switching away from a non-default custom editors and then back, we should continue using the non-default editor', async () => { + const startingContent = `switch, init`; + const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent }); + + const listener = CustomEditorUpdateListener.create(); + + { + await vscode.commands.executeCommand(commands.open, testDocument, { preview: false }); + const { content } = await listener.nextResponse(); + assert.strictEqual(content, startingContent.toString()); + assert.ok(!vscode.window.activeTextEditor); + } + + // Switch to non-default editor + await vscode.commands.executeCommand(commands.openWith, testDocument, 'default', { preview: false }); + assert.strictEqual(vscode.window.activeTextEditor!?.document.uri.toString(), testDocument.toString()); + + // Then open a new document (hiding existing one) + const otherFile = vscode.Uri.file(path.join(testWorkspaceRoot.fsPath, 'other.json')); + await vscode.commands.executeCommand(commands.open, otherFile); + assert.strictEqual(vscode.window.activeTextEditor!?.document.uri.toString(), otherFile.toString()); + + // And then back + await vscode.commands.executeCommand('workbench.action.navigateBack'); + await vscode.commands.executeCommand('workbench.action.navigateBack'); + + // Make sure we have the file on as text + assert.ok(vscode.window.activeTextEditor); + assert.strictEqual(vscode.window.activeTextEditor!?.document.uri.toString(), testDocument.toString()); + }); + + test('Should release the text document when the editor is closed', async () => { + const startingContent = `release document init,`; + const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent }); + + const listener = CustomEditorUpdateListener.create(); + + await vscode.commands.executeCommand(commands.open, testDocument); + await listener.nextResponse(); + + const doc = vscode.workspace.textDocuments.find(x => x.uri.toString() === testDocument.toString()); + assert.ok(doc); + assert.ok(!doc!.isClosed); + + await closeAllEditors(); + await delay(100); + assert.ok(doc!.isClosed); + }); +}); + +async function resetTestWorkspace() { + try { + await vscode.workspace.fs.delete(testWorkspaceRoot, { recursive: true }); + } catch { + // ok if file doesn't exist + } + await vscode.workspace.fs.createDirectory(testWorkspaceRoot); +} diff --git a/extensions/vscode-custom-editor-tests/src/test/index.ts b/extensions/vscode-custom-editor-tests/src/test/index.ts new file mode 100644 index 00000000000..6d80cca8048 --- /dev/null +++ b/extensions/vscode-custom-editor-tests/src/test/index.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. + *--------------------------------------------------------------------------------------------*/ + +const path = require('path'); +const testRunner = require('vscode/lib/testrunner'); + +const suite = 'Custom Editor Tests'; + +const options: any = { + ui: 'tdd', + useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), + timeout: 6000000 +}; + +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`) + } + }; +} + +testRunner.configure(options); + +export = testRunner; diff --git a/extensions/vscode-custom-editor-tests/src/test/utils.ts b/extensions/vscode-custom-editor-tests/src/test/utils.ts new file mode 100644 index 00000000000..5edd53b27cc --- /dev/null +++ b/extensions/vscode-custom-editor-tests/src/test/utils.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +export function randomFilePath(args: { root: vscode.Uri, ext: string }): vscode.Uri { + const fileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10); + return (vscode.Uri as any).joinPath(args.root, fileName + args.ext); +} + +export function closeAllEditors(): Thenable { + return vscode.commands.executeCommand('workbench.action.closeAllEditors'); +} + +export function disposeAll(disposables: vscode.Disposable[]) { + vscode.Disposable.from(...disposables).dispose(); +} + +export function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/extensions/github-browser/src/typings/ref.d.ts b/extensions/vscode-custom-editor-tests/src/typings/ref.d.ts similarity index 66% rename from extensions/github-browser/src/typings/ref.d.ts rename to extensions/vscode-custom-editor-tests/src/typings/ref.d.ts index 312efe5a30f..bf67b19225d 100644 --- a/extensions/github-browser/src/typings/ref.d.ts +++ b/extensions/vscode-custom-editor-tests/src/typings/ref.d.ts @@ -3,6 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/// -/// -/// +/// +/// + diff --git a/extensions/github-browser/tsconfig.json b/extensions/vscode-custom-editor-tests/tsconfig.json similarity index 61% rename from extensions/github-browser/tsconfig.json rename to extensions/vscode-custom-editor-tests/tsconfig.json index eb413a12602..296ddb38fcb 100644 --- a/extensions/github-browser/tsconfig.json +++ b/extensions/vscode-custom-editor-tests/tsconfig.json @@ -1,14 +1,9 @@ { "extends": "../shared.tsconfig.json", "compilerOptions": { - "experimentalDecorators": true, - "lib": [ - "es2018", - "dom" - ], "outDir": "./out" }, "include": [ "src/**/*" ] -} +} \ No newline at end of file diff --git a/extensions/vscode-custom-editor-tests/yarn.lock b/extensions/vscode-custom-editor-tests/yarn.lock new file mode 100644 index 00000000000..0d39ebbaa5e --- /dev/null +++ b/extensions/vscode-custom-editor-tests/yarn.lock @@ -0,0 +1,507 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + +"@types/node@^12.11.7": + version "12.12.53" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.53.tgz#be0d375933c3d15ef2380dafb3b0350ea7021129" + integrity sha512-51MYTDTyCziHb70wtGNFRwB4l+5JNvdqzFSkbDvpbftEgVUBEE+T5f7pROhWMp/fxp07oNIEQZd5bbfAH22ohQ== + +"@types/p-limit@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/p-limit/-/p-limit-2.2.0.tgz#94a608e9b258a6c6156a13d1a14fd720dba70b97" + integrity sha512-fGFbybl1r0oE9mqgfc2EHHUin9ZL5rbQIexWI6jYRU1ADVn4I3LHzT+g/kpPpZsfp8PB94CQ655pfAjNF8LP6A== + dependencies: + p-limit "*" + +agent-base@4, agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + +agent-base@6: + version "6.0.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4" + integrity sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg== + dependencies: + debug "4" + +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= + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +charenc@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + +commander@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06" + integrity sha1-+mihT2qUXVTbvlDYzbMyDp47GgY= + +commander@2.15.1: + version "2.15.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" + integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== + +commander@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.3.0.tgz#fd430e889832ec353b9acd1de217c11cb3eef873" + integrity sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM= + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +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.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + integrity sha1-+HBX6ZWxofauaklgZkE3vFbwOdo= + dependencies: + ms "0.7.1" + +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +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" + +diff@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" + integrity sha1-fyjS657nsVqX79ic5j3P2qPMur8= + +diff@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + +escape-string-regexp@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz#4dbc2fe674e71949caf3fb2695ce7f2dc1d9a8d1" + integrity sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE= + +escape-string-regexp@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +glob@3.2.11: + version "3.2.11" + resolved "https://registry.yarnpkg.com/glob/-/glob-3.2.11.tgz#4a973f635b9190f715d10987d5c00fd2815ebe3d" + integrity sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0= + dependencies: + inherits "2" + minimatch "0.3" + +glob@7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.2: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +growl@1.9.2: + version "1.9.2" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" + integrity sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8= + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +he@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= + +http-proxy-agent@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" + integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg== + dependencies: + agent-base "4" + debug "3.1.0" + +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + +https-proxy-agent@^2.2.1: + version "2.2.4" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" + integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + +https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +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== + +jade@0.26.3: + version "0.26.3" + resolved "https://registry.yarnpkg.com/jade/-/jade-0.26.3.tgz#8f10d7977d8d79f2f6ff862a81b0513ccb25686c" + integrity sha1-jxDXl32NefL2/4YqgbBRPMslaGw= + dependencies: + commander "0.6.1" + mkdirp "0.3.0" + +lodash@^4.16.4: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== + +lru-cache@2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" + integrity sha1-bUUk6LlV+V1PW1iFHOId1y+06VI= + +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" + +minimatch@0.3: + version "0.3.0" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd" + integrity sha1-J12O2qxPG7MyZHIInnlJyDlGmd0= + dependencies: + lru-cache "2" + sigmund "~1.0.0" + +minimatch@3.0.4, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +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.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" + integrity sha1-G79asbqCevI1dRQ0kEJkVfSB/h4= + +mkdirp@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +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" + +mocha@^2.3.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58" + integrity sha1-FhvlvetJZ3HrmzV0UFC2IrWu/Fg= + dependencies: + commander "2.3.0" + debug "2.2.0" + diff "1.4.0" + escape-string-regexp "1.0.2" + glob "3.2.11" + growl "1.9.2" + jade "0.26.3" + mkdirp "0.5.1" + supports-color "1.2.0" + to-iso-string "0.0.2" + +mocha@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" + integrity sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ== + dependencies: + browser-stdout "1.3.1" + commander "2.15.1" + debug "3.1.0" + diff "3.5.0" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.5" + he "1.1.1" + minimatch "3.0.4" + mkdirp "0.5.1" + supports-color "5.4.0" + +ms@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + integrity sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg= + +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== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +p-limit@*, p-limit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe" + integrity sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg== + dependencies: + p-try "^2.0.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +semver@^5.4.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +sigmund@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" + integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= + +source-map-support@^0.5.0: + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +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" + +supports-color@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-1.2.0.tgz#ff1ed1e61169d06b3cf2d588e188b18d8847e17e" + integrity sha1-/x7R5hFp0Gs88tWI4YixjYhH4X4= + +supports-color@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" + integrity sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w== + dependencies: + has-flag "^3.0.0" + +to-iso-string@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/to-iso-string/-/to-iso-string-0.0.2.tgz#4dc19e664dfccbe25bd8db508b00c6da158255d1" + integrity sha1-TcGeZk38y+Jb2NtQiwDG2hWCVdE= + +vscode-test@^0.4.1: + version "0.4.3" + resolved "https://registry.yarnpkg.com/vscode-test/-/vscode-test-0.4.3.tgz#461ebf25fc4bc93d77d982aed556658a2e2b90b8" + integrity sha512-EkMGqBSefZH2MgW65nY05rdRSko15uvzq4VAPM5jVmwYuFQKE7eikKXNJDRxL+OITXHB6pI+a3XqqD32Y3KC5w== + dependencies: + http-proxy-agent "^2.1.0" + https-proxy-agent "^2.2.1" + +vscode@^1.1.36: + version "1.1.37" + resolved "https://registry.yarnpkg.com/vscode/-/vscode-1.1.37.tgz#c2a770bee4bb3fff765e2b72c7bcc813b8a6bb0a" + integrity sha512-vJNj6IlN7IJPdMavlQa1KoFB3Ihn06q1AiN3ZFI/HfzPNzbKZWPPuiU+XkpNOfGU5k15m4r80nxNPlM7wcc0wg== + dependencies: + glob "^7.1.2" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + mocha "^5.2.0" + semver "^5.4.1" + source-map-support "^0.5.0" + vscode-test "^0.4.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +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/vscode-notebook-tests/src/notebook.test.ts b/extensions/vscode-notebook-tests/src/notebook.test.ts index 55862568d13..51e5d6ac0ce 100644 --- a/extensions/vscode-notebook-tests/src/notebook.test.ts +++ b/extensions/vscode-notebook-tests/src/notebook.test.ts @@ -6,7 +6,7 @@ import 'mocha'; import * as assert from 'assert'; import * as vscode from 'vscode'; -import { join } from 'path'; +import { createRandomFile } from './utils'; export function timeoutAsync(n: number): Promise { return new Promise(resolve => { @@ -56,6 +56,42 @@ async function splitEditor() { await once; } +async function saveFileAndCloseAll(resource: vscode.Uri) { + const documentClosed = new Promise((resolve, _reject) => { + const d = vscode.notebook.onDidCloseNotebookDocument(e => { + if (e.uri.toString() === resource.toString()) { + d.dispose(); + resolve(); + } + }); + }); + await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await documentClosed; +} + +async function saveAllFilesAndCloseAll(resource: vscode.Uri) { + const documentClosed = new Promise((resolve, _reject) => { + const d = vscode.notebook.onDidCloseNotebookDocument(e => { + if (e.uri.toString() === resource.toString()) { + d.dispose(); + resolve(); + } + }); + }); + await vscode.commands.executeCommand('workbench.action.files.saveAll'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await documentClosed; +} + +function assertInitalState() { + // no-op unless we figure out why some documents are opened after the editor is closed + + // assert.equal(vscode.notebook.activeNotebookEditor, undefined); + // assert.equal(vscode.notebook.notebookDocuments.length, 0); + // assert.equal(vscode.notebook.visibleNotebookEditors.length, 0); +} + suite('Notebook API tests', () => { // test.only('crash', async function () { // for (let i = 0; i < 200; i++) { @@ -83,7 +119,9 @@ suite('Notebook API tests', () => { // }); test('document open/close event', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); const firstDocumentOpen = getEventOncePromise(vscode.notebook.onDidOpenNotebookDocument); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await firstDocumentOpen; @@ -134,7 +172,9 @@ suite('Notebook API tests', () => { }); test('shared document in notebook editors', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); let counter = 0; const disposables: vscode.Disposable[] = []; disposables.push(vscode.notebook.onDidOpenNotebookDocument(() => { @@ -155,7 +195,9 @@ suite('Notebook API tests', () => { }); test('editor open/close event', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); const firstEditorOpen = getEventOncePromise(vscode.notebook.onDidChangeVisibleNotebookEditors); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await firstEditorOpen; @@ -166,7 +208,9 @@ suite('Notebook API tests', () => { }); test('editor open/close event 2', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); let count = 0; const disposables: vscode.Disposable[] = []; disposables.push(vscode.notebook.onDidChangeVisibleNotebookEditors(() => { @@ -184,7 +228,9 @@ suite('Notebook API tests', () => { }); test('editor editing event 2', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); const cellsChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); @@ -256,7 +302,8 @@ suite('Notebook API tests', () => { }); test('editor move cell event', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); @@ -297,7 +344,8 @@ suite('Notebook API tests', () => { }); test('notebook editor active/visible', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); const firstEditor = vscode.notebook.activeNotebookEditor; assert.equal(firstEditor?.active, true); @@ -332,7 +380,8 @@ suite('Notebook API tests', () => { }); test('notebook active editor change', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); const firstEditorOpen = getEventOncePromise(vscode.notebook.onDidChangeActiveNotebookEditor); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await firstEditorOpen; @@ -341,12 +390,12 @@ suite('Notebook API tests', () => { await vscode.commands.executeCommand('workbench.action.splitEditor'); await firstEditorDeactivate; - await vscode.commands.executeCommand('workbench.action.files.save'); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await saveFileAndCloseAll(resource); }); test('edit API', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); const cellsChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); @@ -361,12 +410,12 @@ suite('Notebook API tests', () => { assert.deepEqual(cellChangeEventRet.changes[0].deletedCount, 0); assert.equal(cellChangeEventRet.changes[0].items[0], vscode.notebook.activeNotebookEditor!.document.cells[1]); - await vscode.commands.executeCommand('workbench.action.files.save'); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await saveFileAndCloseAll(resource); }); test('initialzation should not emit cell change events.', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); let count = 0; const disposables: vscode.Disposable[] = []; @@ -378,14 +427,15 @@ suite('Notebook API tests', () => { assert.equal(count, 0); disposables.forEach(d => d.dispose()); - await vscode.commands.executeCommand('workbench.action.files.save'); - await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + + await saveFileAndCloseAll(resource); }); }); suite('notebook workflow', () => { test('notebook open', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), 'test'); @@ -406,7 +456,8 @@ suite('notebook workflow', () => { }); test('notebook cell actions', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), 'test'); @@ -479,7 +530,8 @@ suite('notebook workflow', () => { }); test('notebook join cells', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), 'test'); @@ -487,7 +539,9 @@ suite('notebook workflow', () => { await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), ''); - await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + const edit = new vscode.WorkspaceEdit(); + edit.insert(vscode.notebook.activeNotebookEditor!.selection!.uri, new vscode.Position(0, 0), 'var abc = 0;'); + await vscode.workspace.applyEdit(edit); const cellsChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); await vscode.commands.executeCommand('notebook.cell.joinAbove'); @@ -500,7 +554,8 @@ suite('notebook workflow', () => { }); test('move cells will not recreate cells in ExtHost', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); @@ -513,15 +568,14 @@ suite('notebook workflow', () => { const newActiveCell = vscode.notebook.activeNotebookEditor!.selection; assert.deepEqual(activeCell, newActiveCell); - await vscode.commands.executeCommand('workbench.action.files.saveAll'); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await saveFileAndCloseAll(resource); // TODO@rebornix, there are still some events order issue. // assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(newActiveCell!), 2); }); // test.only('document metadata is respected', async function () { - // const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + // const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); // await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); // assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); @@ -545,7 +599,8 @@ suite('notebook workflow', () => { // }); test('cell runnable metadata is respected', async () => { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); const editor = vscode.notebook.activeNotebookEditor!; @@ -566,7 +621,8 @@ suite('notebook workflow', () => { }); test('document runnable metadata is respected', async () => { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); const editor = vscode.notebook.activeNotebookEditor!; @@ -588,7 +644,8 @@ suite('notebook workflow', () => { suite('notebook dirty state', () => { test('notebook open', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), 'test'); @@ -604,25 +661,22 @@ suite('notebook dirty state', () => { assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.length, 3); assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(activeCell!), 1); - - await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); - await vscode.commands.executeCommand('workbench.action.files.newUntitledFile'); - await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); - - await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + const edit = new vscode.WorkspaceEdit(); + edit.insert(activeCell!.uri, new vscode.Position(0, 0), 'var abc = 0;'); + await vscode.workspace.applyEdit(edit); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true); assert.equal(vscode.notebook.activeNotebookEditor?.selection !== undefined, true); assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells[1], vscode.notebook.activeNotebookEditor?.selection); assert.equal(vscode.notebook.activeNotebookEditor?.selection?.document.getText(), 'var abc = 0;'); - await vscode.commands.executeCommand('workbench.action.files.save'); - await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + await saveFileAndCloseAll(resource); }); }); suite('notebook undo redo', () => { test('notebook open', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), 'test'); @@ -640,14 +694,16 @@ suite('notebook undo redo', () => { // modify the second cell, delete it - await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + const edit = new vscode.WorkspaceEdit(); + edit.insert(vscode.notebook.activeNotebookEditor!.selection!.uri, new vscode.Position(0, 0), 'var abc = 0;'); + await vscode.workspace.applyEdit(edit); await vscode.commands.executeCommand('notebook.cell.delete'); assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.length, 2); assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(vscode.notebook.activeNotebookEditor!.selection!), 1); // undo should bring back the deleted cell, and revert to previous content and selection - await vscode.commands.executeCommand('notebook.undo'); + await vscode.commands.executeCommand('undo'); assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.length, 3); assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(vscode.notebook.activeNotebookEditor!.selection!), 1); assert.equal(vscode.notebook.activeNotebookEditor?.selection?.document.getText(), 'var abc = 0;'); @@ -658,12 +714,12 @@ suite('notebook undo redo', () => { // assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(vscode.notebook.activeNotebookEditor!.selection!), 1); // assert.equal(vscode.notebook.activeNotebookEditor?.selection?.document.getText(), 'test'); - await vscode.commands.executeCommand('workbench.action.files.save'); - await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + await saveFileAndCloseAll(resource); }); test.skip('execute and then undo redo', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); const cellsChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); @@ -713,7 +769,7 @@ suite('notebook undo redo', () => { assert.equal(cellOutputsAddedRet.cells[0].outputs.length, 1); const cellOutputClear = getEventOncePromise(vscode.notebook.onDidChangeCellOutputs); - await vscode.commands.executeCommand('notebook.undo'); + await vscode.commands.executeCommand('undo'); const cellOutputsCleardRet = await cellOutputClear; assert.deepEqual(cellOutputsCleardRet, { document: vscode.notebook.activeNotebookEditor!.document, @@ -721,15 +777,14 @@ suite('notebook undo redo', () => { }); assert.equal(cellOutputsAddedRet.cells[0].outputs.length, 0); - await vscode.commands.executeCommand('workbench.action.files.save'); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await saveFileAndCloseAll(resource); }); }); suite('notebook working copy', () => { // test('notebook revert on close', async function () { - // const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + // const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); // await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); // await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); // assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), ''); @@ -750,7 +805,7 @@ suite('notebook working copy', () => { // }); // test('notebook revert', async function () { - // const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + // const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); // await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); // await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); // assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), ''); @@ -770,15 +825,18 @@ suite('notebook working copy', () => { // }); test('multiple tabs: dirty + clean', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), ''); await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); - await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + const edit = new vscode.WorkspaceEdit(); + edit.insert(vscode.notebook.activeNotebookEditor!.selection!.uri, new vscode.Position(0, 0), 'var abc = 0;'); + await vscode.workspace.applyEdit(edit); - const secondResource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './second.vsctestnb')); + const secondResource = await createRandomFile('', undefined, 'second', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', secondResource, 'notebookCoreTest'); await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); @@ -789,20 +847,22 @@ suite('notebook working copy', () => { assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells.length, 3); assert.equal(vscode.notebook.activeNotebookEditor?.selection?.document.getText(), 'var abc = 0;'); - await vscode.commands.executeCommand('workbench.action.files.save'); - await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + await saveFileAndCloseAll(resource); }); test('multiple tabs: two dirty tabs and switching', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), ''); await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); - await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + const edit = new vscode.WorkspaceEdit(); + edit.insert(vscode.notebook.activeNotebookEditor!.selection!.uri, new vscode.Position(0, 0), 'var abc = 0;'); + await vscode.workspace.applyEdit(edit); - const secondResource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './second.vsctestnb')); + const secondResource = await createRandomFile('', undefined, 'second', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', secondResource, 'notebookCoreTest'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), ''); @@ -823,13 +883,15 @@ suite('notebook working copy', () => { assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells.length, 2); assert.equal(vscode.notebook.activeNotebookEditor?.selection?.document.getText(), ''); - await vscode.commands.executeCommand('workbench.action.files.saveAll'); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await saveAllFilesAndCloseAll(secondResource); + // await vscode.commands.executeCommand('workbench.action.files.saveAll'); + // await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); test('multiple tabs: different editors with same document', async function () { + assertInitalState(); - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); const firstNotebookEditor = vscode.notebook.activeNotebookEditor; assert.equal(firstNotebookEditor !== undefined, true, 'notebook first'); @@ -846,28 +908,31 @@ suite('notebook working copy', () => { assert.equal(firstNotebookEditor?.document, secondNotebookEditor?.document, 'split notebook editors share the same document'); assert.notEqual(firstNotebookEditor?.asWebviewUri(vscode.Uri.file('./hello.png')), secondNotebookEditor?.asWebviewUri(vscode.Uri.file('./hello.png'))); - await vscode.commands.executeCommand('workbench.action.files.saveAll'); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await saveAllFilesAndCloseAll(resource); + + // await vscode.commands.executeCommand('workbench.action.files.saveAll'); + // await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); }); suite('metadata', () => { test('custom metadata should be supported', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.document.metadata.custom!['testMetadata'] as boolean, false); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.metadata.custom!['testCellMetadata'] as number, 123); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.language, 'typescript'); - await vscode.commands.executeCommand('workbench.action.files.saveAll'); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await saveFileAndCloseAll(resource); }); // TODO@rebornix skip as it crashes the process all the time - test.skip('custom metadata should be supported', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + test.skip('custom metadata should be supported 2', async function () { + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.document.metadata.custom!['testMetadata'] as boolean, false); @@ -880,27 +945,29 @@ suite('metadata', () => { // assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(activeCell!), 1); // assert.equal(activeCell?.metadata.custom!['testCellMetadata'] as number, 123); - await vscode.commands.executeCommand('workbench.action.files.saveAll'); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await saveFileAndCloseAll(resource); }); }); suite('regression', () => { test('microsoft/vscode-github-issue-notebooks#26. Insert template cell in the new empty document', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './empty.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'empty', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), ''); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.language, 'typescript'); - await vscode.commands.executeCommand('workbench.action.files.saveAll'); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await saveFileAndCloseAll(resource); }); test('#97830, #97764. Support switch to other editor types', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './empty.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'empty', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); - await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + const edit = new vscode.WorkspaceEdit(); + edit.insert(vscode.notebook.activeNotebookEditor!.selection!.uri, new vscode.Position(0, 0), 'var abc = 0;'); + await vscode.workspace.applyEdit(edit); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), 'var abc = 0;'); @@ -909,33 +976,37 @@ suite('regression', () => { await vscode.commands.executeCommand('vscode.openWith', resource, 'default'); assert.equal(vscode.window.activeTextEditor?.document.uri.path, resource.path); - await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor'); await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); // open text editor, pin, and then open a notebook test('#96105 - dirty editors', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './empty.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'empty', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'default'); - await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); - await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + const edit = new vscode.WorkspaceEdit(); + edit.insert(resource, new vscode.Position(0, 0), 'var abc = 0;'); + await vscode.workspace.applyEdit(edit); // now it's dirty, open the resource with notebook editor should open a new one await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.notEqual(vscode.notebook.activeNotebookEditor, undefined, 'notebook first'); assert.notEqual(vscode.window.activeTextEditor, undefined); - await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor'); await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); test('#102411 - untitled notebook creation failed', async function () { + assertInitalState(); await vscode.commands.executeCommand('workbench.action.files.newUntitledFile', { viewType: 'notebookCoreTest' }); assert.notEqual(vscode.notebook.activeNotebookEditor, undefined, 'untitled notebook editor is not undefined'); + + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); test('#102423 - copy/paste shares the same text buffer', async function () { - const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); let activeCell = vscode.notebook.activeNotebookEditor!.selection; @@ -947,10 +1018,14 @@ suite('regression', () => { assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(activeCell!), 1); assert.equal(activeCell?.document.getText(), 'test'); - await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + const edit = new vscode.WorkspaceEdit(); + edit.insert(vscode.notebook.activeNotebookEditor!.selection!.uri, new vscode.Position(0, 0), 'var abc = 0;'); + await vscode.workspace.applyEdit(edit); assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.length, 2); assert.notEqual(vscode.notebook.activeNotebookEditor!.document.cells[0].document.getText(), vscode.notebook.activeNotebookEditor!.document.cells[1].document.getText()); + + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); }); @@ -961,11 +1036,11 @@ suite('webview', () => { // return; // } - // const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + // const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); // await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); // assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); // const uri = vscode.notebook.activeNotebookEditor!.asWebviewUri(vscode.Uri.file('./hello.png')); - // assert.equal(uri.scheme, 'vscode-resource'); + // assert.equal(uri.scheme, 'vscode-webview-resource'); // await vscode.commands.executeCommand('workbench.action.closeAllEditors'); // }); diff --git a/extensions/vscode-notebook-tests/src/notebookSmokeTestMain.ts b/extensions/vscode-notebook-tests/src/notebookSmokeTestMain.ts index 8c32dedc3bf..0cec3fe06d6 100644 --- a/extensions/vscode-notebook-tests/src/notebookSmokeTestMain.ts +++ b/extensions/vscode-notebook-tests/src/notebookSmokeTestMain.ts @@ -67,7 +67,7 @@ export function smokeTestActivate(context: vscode.ExtensionContext): any { } })); - context.subscriptions.push(vscode.notebook.registerNotebookKernel('notebookSmokeTest', ['*.vsctestnb'], { + context.subscriptions.push(vscode.notebook.registerNotebookKernel('notebookSmokeTest', ['*.smoke-nb'], { label: 'notebookSmokeTest', executeAllCells: async (_document: vscode.NotebookDocument) => { for (let i = 0; i < _document.cells.length; i++) { @@ -79,7 +79,8 @@ export function smokeTestActivate(context: vscode.ExtensionContext): any { }]; } }, - executeCell: async (_document: vscode.NotebookDocument, _cell: vscode.NotebookCell | undefined, _token: vscode.CancellationToken) => { + cancelAllCellsExecution: async () => { }, + executeCell: async (_document: vscode.NotebookDocument, _cell: vscode.NotebookCell | undefined) => { if (!_cell) { _cell = _document.cells[0]; } @@ -92,6 +93,7 @@ export function smokeTestActivate(context: vscode.ExtensionContext): any { }]; return; }, + cancelCellExecution: async () => { } })); 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 33dd2960fda..c8ea3282c2c 100644 --- a/extensions/vscode-notebook-tests/src/notebookTestMain.ts +++ b/extensions/vscode-notebook-tests/src/notebookTestMain.ts @@ -15,7 +15,7 @@ export function activate(context: vscode.ExtensionContext): any { context.subscriptions.push(vscode.notebook.registerNotebookContentProvider('notebookCoreTest', { onDidChangeNotebook: _onDidChangeNotebook.event, openNotebook: async (_resource: vscode.Uri) => { - if (_resource.path.endsWith('empty.vsctestnb')) { + if (/.*empty\-.*\.vsctestnb$/.test(_resource.path)) { return { languages: ['typescript'], metadata: {}, @@ -62,8 +62,8 @@ export function activate(context: vscode.ExtensionContext): any { context.subscriptions.push(vscode.notebook.registerNotebookKernel('notebookKernelTest', ['*.vsctestnb'], { label: 'Notebook Test Kernel', - executeAllCells: async (_document: vscode.NotebookDocument, _token: vscode.CancellationToken) => { - let cell = _document.cells[0]; + executeAllCells: async (_document: vscode.NotebookDocument) => { + const cell = _document.cells[0]; cell.outputs = [{ outputKind: vscode.CellOutputKind.Rich, @@ -73,7 +73,8 @@ export function activate(context: vscode.ExtensionContext): any { }]; return; }, - executeCell: async (document: vscode.NotebookDocument, cell: vscode.NotebookCell | undefined, _token: vscode.CancellationToken) => { + cancelAllCellsExecution: async (_document: vscode.NotebookDocument) => { }, + executeCell: async (document: vscode.NotebookDocument, cell: vscode.NotebookCell | undefined) => { if (!cell) { cell = document.cells[0]; } @@ -113,7 +114,8 @@ 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')); diff --git a/extensions/vscode-notebook-tests/src/utils.ts b/extensions/vscode-notebook-tests/src/utils.ts new file mode 100644 index 00000000000..0a8f20a1942 --- /dev/null +++ b/extensions/vscode-notebook-tests/src/utils.ts @@ -0,0 +1,261 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import * as path from 'path'; +import * as vscode from 'vscode'; + +class File implements vscode.FileStat { + + type: vscode.FileType; + ctime: number; + mtime: number; + size: number; + + name: string; + data?: Uint8Array; + + constructor(name: string) { + this.type = vscode.FileType.File; + this.ctime = Date.now(); + this.mtime = Date.now(); + this.size = 0; + this.name = name; + } +} + +class Directory implements vscode.FileStat { + + type: vscode.FileType; + ctime: number; + mtime: number; + size: number; + + name: string; + entries: Map; + + constructor(name: string) { + this.type = vscode.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; + +export class TestFS implements vscode.FileSystemProvider { + + constructor( + readonly scheme: string, + readonly isCaseSensitive: boolean + ) { } + + readonly root = new Directory(''); + + // --- manage file metadata + + stat(uri: vscode.Uri): vscode.FileStat { + return this._lookup(uri, false); + } + + readDirectory(uri: vscode.Uri): [string, vscode.FileType][] { + const entry = this._lookupAsDirectory(uri, false); + const result: [string, vscode.FileType][] = []; + for (const [name, child] of entry.entries) { + result.push([name, child.type]); + } + return result; + } + + // --- manage file contents + + readFile(uri: vscode.Uri): Uint8Array { + const data = this._lookupAsFile(uri, false).data; + if (data) { + return data; + } + throw vscode.FileSystemError.FileNotFound(); + } + + writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): void { + const basename = path.posix.basename(uri.path); + const parent = this._lookupParentDirectory(uri); + let entry = parent.entries.get(basename); + if (entry instanceof Directory) { + throw vscode.FileSystemError.FileIsADirectory(uri); + } + if (!entry && !options.create) { + throw vscode.FileSystemError.FileNotFound(uri); + } + if (entry && options.create && !options.overwrite) { + throw vscode.FileSystemError.FileExists(uri); + } + if (!entry) { + entry = new File(basename); + parent.entries.set(basename, entry); + this._fireSoon({ type: vscode.FileChangeType.Created, uri }); + } + entry.mtime = Date.now(); + entry.size = content.byteLength; + entry.data = content; + + this._fireSoon({ type: vscode.FileChangeType.Changed, uri }); + } + + // --- manage files/folders + + rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean }): void { + + if (!options.overwrite && this._lookup(newUri, true)) { + throw vscode.FileSystemError.FileExists(newUri); + } + + const entry = this._lookup(oldUri, false); + const oldParent = this._lookupParentDirectory(oldUri); + + const newParent = this._lookupParentDirectory(newUri); + const newName = path.posix.basename(newUri.path); + + oldParent.entries.delete(entry.name); + entry.name = newName; + newParent.entries.set(newName, entry); + + this._fireSoon( + { type: vscode.FileChangeType.Deleted, uri: oldUri }, + { type: vscode.FileChangeType.Created, uri: newUri } + ); + } + + delete(uri: vscode.Uri): void { + const dirname = uri.with({ path: path.posix.dirname(uri.path) }); + const basename = path.posix.basename(uri.path); + const parent = this._lookupAsDirectory(dirname, false); + if (!parent.entries.has(basename)) { + throw vscode.FileSystemError.FileNotFound(uri); + } + parent.entries.delete(basename); + parent.mtime = Date.now(); + parent.size -= 1; + this._fireSoon({ type: vscode.FileChangeType.Changed, uri: dirname }, { uri, type: vscode.FileChangeType.Deleted }); + } + + createDirectory(uri: vscode.Uri): void { + const basename = path.posix.basename(uri.path); + const dirname = uri.with({ path: path.posix.dirname(uri.path) }); + const parent = this._lookupAsDirectory(dirname, false); + + const entry = new Directory(basename); + parent.entries.set(entry.name, entry); + parent.mtime = Date.now(); + parent.size += 1; + this._fireSoon({ type: vscode.FileChangeType.Changed, uri: dirname }, { type: vscode.FileChangeType.Created, uri }); + } + + // --- lookup + + private _lookup(uri: vscode.Uri, silent: false): Entry; + private _lookup(uri: vscode.Uri, silent: boolean): Entry | undefined; + private _lookup(uri: vscode.Uri, silent: boolean): Entry | undefined { + const parts = uri.path.split('/'); + let entry: Entry = this.root; + for (const part of parts) { + const partLow = part.toLowerCase(); + if (!part) { + continue; + } + let child: Entry | undefined; + if (entry instanceof Directory) { + if (this.isCaseSensitive) { + child = entry.entries.get(part); + } else { + for (const [key, value] of entry.entries) { + if (key.toLowerCase() === partLow) { + child = value; + break; + } + } + } + } + if (!child) { + if (!silent) { + throw vscode.FileSystemError.FileNotFound(uri); + } else { + return undefined; + } + } + entry = child; + } + return entry; + } + + private _lookupAsDirectory(uri: vscode.Uri, silent: boolean): Directory { + const entry = this._lookup(uri, silent); + if (entry instanceof Directory) { + return entry; + } + throw vscode.FileSystemError.FileNotADirectory(uri); + } + + private _lookupAsFile(uri: vscode.Uri, silent: boolean): File { + const entry = this._lookup(uri, silent); + if (entry instanceof File) { + return entry; + } + throw vscode.FileSystemError.FileIsADirectory(uri); + } + + private _lookupParentDirectory(uri: vscode.Uri): Directory { + const dirname = uri.with({ path: path.posix.dirname(uri.path) }); + return this._lookupAsDirectory(dirname, false); + } + + // --- manage file events + + private _emitter = new vscode.EventEmitter(); + private _bufferedEvents: vscode.FileChangeEvent[] = []; + private _fireSoonHandle?: NodeJS.Timer; + + readonly onDidChangeFile: vscode.Event = this._emitter.event; + + watch(_resource: vscode.Uri): vscode.Disposable { + // ignore, fires for all changes... + return new vscode.Disposable(() => { }); + } + + private _fireSoon(...events: vscode.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); + } +} + +export function rndName() { + return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10); +} + +export const testFs = new TestFS('fake-fs', true); +vscode.workspace.registerFileSystemProvider(testFs.scheme, testFs, { isCaseSensitive: testFs.isCaseSensitive }); + +export async function createRandomFile(contents = '', dir: vscode.Uri | undefined = undefined, prefix = '', ext = ''): Promise { + let fakeFile: vscode.Uri; + if (dir) { + fakeFile = dir.with({ path: dir.path + '/' + rndName() + ext }); + } else { + fakeFile = vscode.Uri.parse(`${testFs.scheme}:/${prefix}-${rndName() + ext}`); + } + + await testFs.writeFile(fakeFile, Buffer.from(contents), { create: true, overwrite: true }); + return fakeFile; +} diff --git a/extensions/yarn.lock b/extensions/yarn.lock index d41a4ab48b5..102d128edb6 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,86 +2,7 @@ # yarn lockfile v1 -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -glob@^7.1.3: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -typescript@3.9.6: - version "3.9.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a" - integrity sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw== - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +typescript@3.9.7: + version "3.9.7" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" + integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== diff --git a/package.json b/package.json index 45ac42b7869..46b582a2a37 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", - "version": "1.48.0", - "distro": "b61767b72450bb972954ac50b830825554a107dc", + "version": "1.49.0", + "distro": "d368861bfb088f58e20fd0ebbcaeac0104e81f80", "author": { "name": "Microsoft Corporation" }, @@ -62,6 +62,7 @@ "semver-umd": "^5.5.7", "spdlog": "^0.11.1", "sudo-prompt": "9.1.1", + "tas-client": "^0.0.950", "v8-inspect-profiler": "^0.0.20", "vscode-nsfw": "1.2.8", "vscode-oniguruma": "1.3.1", @@ -69,10 +70,10 @@ "vscode-ripgrep": "^1.8.0", "vscode-sqlite3": "4.0.10", "vscode-textmate": "5.2.0", - "xterm": "4.8.1", + "xterm": "4.9.0-beta.8", "xterm-addon-search": "0.7.0", "xterm-addon-unicode11": "0.2.0", - "xterm-addon-webgl": "0.7.0", + "xterm-addon-webgl": "0.8.0", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, @@ -108,7 +109,7 @@ "css-loader": "^3.2.0", "debounce": "^1.0.0", "deemon": "^1.4.0", - "electron": "8.3.3", + "electron": "7.3.2", "eslint": "6.8.0", "eslint-plugin-jsdoc": "^19.1.0", "event-stream": "3.3.4", @@ -163,7 +164,7 @@ "source-map": "^0.4.4", "style-loader": "^1.0.0", "ts-loader": "^4.4.2", - "typescript": "^4.0.0-dev.20200715", + "typescript": "^4.0.0-dev.20200803", "typescript-formatter": "7.1.0", "underscore": "^1.8.2", "vinyl": "^2.0.0", @@ -190,4 +191,4 @@ "windows-mutex": "0.3.0", "windows-process-tree": "0.2.4" } -} +} \ No newline at end of file diff --git a/product.json b/product.json index 94b2c9862cc..efbb04d8b55 100644 --- a/product.json +++ b/product.json @@ -25,12 +25,13 @@ "extensionAllowedProposedApi": [ "ms-vscode.vscode-js-profile-flame", "ms-vscode.vscode-js-profile-table", - "ms-vscode.references-view" + "ms-vscode.references-view", + "ms-vscode.github-browser" ], "builtInExtensions": [ { "name": "ms-vscode.node-debug", - "version": "1.44.7", + "version": "1.44.9", "repo": "https://github.com/Microsoft/vscode-node-debug", "metadata": { "id": "b6ded8fb-a0a0-4c1c-acbd-ab2a3bc995a6", @@ -60,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", @@ -90,7 +91,7 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.48.0", + "version": "1.48.1", "repo": "https://github.com/Microsoft/vscode-js-debug", "metadata": { "id": "25629058-ddac-4e17-abba-74678e126c5d", @@ -118,5 +119,22 @@ "publisherDisplayName": "Microsoft" } } + ], + "webBuiltInExtensions": [ + { + "name": "ms-vscode.github-browser", + "version": "0.0.2", + "repo": "https://github.com/Microsoft/vscode-github-browser", + "metadata": { + "id": "c1bcff4b-4ecb-466e-b8f6-b02788b5fb5a", + "publisherId": { + "publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee", + "publisherName": "ms-vscode", + "displayName": "Microsoft", + "flags": "verified" + }, + "publisherDisplayName": "Microsoft" + } + } ] } diff --git a/remote/package.json b/remote/package.json index a31fcd4a1a3..c10fbe792c2 100644 --- a/remote/package.json +++ b/remote/package.json @@ -20,10 +20,10 @@ "vscode-proxy-agent": "^0.5.2", "vscode-ripgrep": "^1.8.0", "vscode-textmate": "5.2.0", - "xterm": "4.8.1", + "xterm": "4.9.0-beta.8", "xterm-addon-search": "0.7.0", "xterm-addon-unicode11": "0.2.0", - "xterm-addon-webgl": "0.7.0", + "xterm-addon-webgl": "0.8.0", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/web/package.json b/remote/web/package.json index 26dfd2b30bd..de712cfdeb3 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -7,9 +7,9 @@ "semver-umd": "^5.5.7", "vscode-oniguruma": "1.3.1", "vscode-textmate": "5.2.0", - "xterm": "4.8.1", + "xterm": "4.9.0-beta.8", "xterm-addon-search": "0.7.0", "xterm-addon-unicode11": "0.2.0", - "xterm-addon-webgl": "0.7.0" + "xterm-addon-webgl": "0.8.0" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index da501df1475..15fb0958eea 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -37,12 +37,12 @@ xterm-addon-unicode11@0.2.0: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.2.0.tgz#9ed0c482b353908bba27778893ca80823382737c" integrity sha512-rjFDItPc/IDoSiEnoDFwKroNwLD/7t9vYKENjrcKVZg5tgJuuUj8D4rZtP6iVCjSB1LTLYmUs4L/EmCqIyLR/Q== -xterm-addon-webgl@0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.7.0.tgz#a13732ac937170e53ce02ec91963da042c80614b" - integrity sha512-PMWLgccAF31GulCYkQxIA8qwMI4q4UbRi5O/zwMnSJWBozB0yy84lX31ZhJeJhcrlEn1Vpcd+OUGPE8Z1hBjnw== +xterm-addon-webgl@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.8.0.tgz#4bc6bb4dbfea5b0d2d7978d6c5cef922d584fb4f" + integrity sha512-dlpYPsv0C9S6v6+T/h/d/otSbdUTizMJdxvSoS34tUpMOHev6iW7Zqt5KRFqYxl4vCqpDk9Wmhb3fKL3kwX5fQ== -xterm@4.8.1: - version "4.8.1" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.8.1.tgz#155a1729a43e1a89b406524e22c5634339e39ca1" - integrity sha512-ax91ny4tI5eklqIfH79OUSGE2PUX2rGbwONmB6DfqpyhSZO8/cf++sqiaMWEVCMjACyMfnISW7C3gGMoNvNolQ== +xterm@4.9.0-beta.8: + version "4.9.0-beta.8" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.9.0-beta.8.tgz#ca121934d63f88668d2d5b11d9b2fc3bde7bd805" + integrity sha512-EEonYBLANDUBfEeEnHG632bZdgBaAUWst8LFr6oC6f2uLFfJGHQvVJuLaEkPtRvS+jOeoorEXZRPmso1/ANHXA== diff --git a/remote/yarn.lock b/remote/yarn.lock index 18f4de33810..499dc73ca5c 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -440,15 +440,15 @@ xterm-addon-unicode11@0.2.0: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.2.0.tgz#9ed0c482b353908bba27778893ca80823382737c" integrity sha512-rjFDItPc/IDoSiEnoDFwKroNwLD/7t9vYKENjrcKVZg5tgJuuUj8D4rZtP6iVCjSB1LTLYmUs4L/EmCqIyLR/Q== -xterm-addon-webgl@0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.7.0.tgz#a13732ac937170e53ce02ec91963da042c80614b" - integrity sha512-PMWLgccAF31GulCYkQxIA8qwMI4q4UbRi5O/zwMnSJWBozB0yy84lX31ZhJeJhcrlEn1Vpcd+OUGPE8Z1hBjnw== +xterm-addon-webgl@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.8.0.tgz#4bc6bb4dbfea5b0d2d7978d6c5cef922d584fb4f" + integrity sha512-dlpYPsv0C9S6v6+T/h/d/otSbdUTizMJdxvSoS34tUpMOHev6iW7Zqt5KRFqYxl4vCqpDk9Wmhb3fKL3kwX5fQ== -xterm@4.8.1: - version "4.8.1" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.8.1.tgz#155a1729a43e1a89b406524e22c5634339e39ca1" - integrity sha512-ax91ny4tI5eklqIfH79OUSGE2PUX2rGbwONmB6DfqpyhSZO8/cf++sqiaMWEVCMjACyMfnISW7C3gGMoNvNolQ== +xterm@4.9.0-beta.8: + version "4.9.0-beta.8" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.9.0-beta.8.tgz#ca121934d63f88668d2d5b11d9b2fc3bde7bd805" + integrity sha512-EEonYBLANDUBfEeEnHG632bZdgBaAUWst8LFr6oC6f2uLFfJGHQvVJuLaEkPtRvS+jOeoorEXZRPmso1/ANHXA== yauzl@^2.9.2: version "2.10.0" diff --git a/resources/serverless/code-web.js b/resources/serverless/code-web.js index 998d88b8842..90d81b3fb9d 100644 --- a/resources/serverless/code-web.js +++ b/resources/serverless/code-web.js @@ -28,7 +28,8 @@ const args = minimist(process.argv, { boolean: [ 'no-launch', 'help', - 'verbose' + 'verbose', + 'wrap-iframe' ], string: [ 'scheme', @@ -43,6 +44,7 @@ if (args.help) { console.log( 'yarn web [options]\n' + ' --no-launch Do not open VSCode web in the browser\n' + + ' --wrap-iframe Wrap the Web Worker Extension Host in an iframe\n' + ' --scheme Protocol (https or http)\n' + ' --host Remote host\n' + ' --port Remote/Local port\n' + @@ -64,56 +66,36 @@ const AUTHORITY = process.env.VSCODE_AUTHORITY || `${HOST}:${PORT}`; const exists = (path) => util.promisify(fs.exists)(path); const readFile = (path) => util.promisify(fs.readFile)(path); -const readdir = (path) => util.promisify(fs.readdir)(path); -const readdirWithFileTypes = (path) => util.promisify(fs.readdir)(path, { withFileTypes: true }); async function getBuiltInExtensionInfos() { - const extensions = []; + const allExtensions = []; /** @type {Object.} */ const locations = {}; - for (const extensionsRoot of [BUILTIN_EXTENSIONS_ROOT, BUILTIN_MARKETPLACE_EXTENSIONS_ROOT]) { - if (await exists(extensionsRoot)) { - const children = await readdirWithFileTypes(extensionsRoot); - await Promise.all(children.map(async child => { - if (child.isDirectory()) { - const extensionPath = path.join(extensionsRoot, child.name); - const info = await getBuiltInExtensionInfo(extensionPath); - if (info) { - extensions.push(info); - locations[path.basename(extensionPath)] = extensionPath; - } - } - })); + const [localExtensions, marketplaceExtensions] = await Promise.all([ + extensions.scanBuiltinExtensions(BUILTIN_EXTENSIONS_ROOT), + extensions.scanBuiltinExtensions(BUILTIN_MARKETPLACE_EXTENSIONS_ROOT), + ]); + for (const ext of localExtensions) { + allExtensions.push(ext); + locations[ext.extensionPath] = path.join(BUILTIN_EXTENSIONS_ROOT, ext.extensionPath); + } + for (const ext of marketplaceExtensions) { + allExtensions.push(ext); + locations[ext.extensionPath] = path.join(BUILTIN_MARKETPLACE_EXTENSIONS_ROOT, ext.extensionPath); + } + for (const ext of allExtensions) { + if (ext.packageJSON.browser) { + let mainFilePath = path.join(locations[ext.extensionPath], ext.packageJSON.browser); + if (path.extname(mainFilePath) !== '.js') { + mainFilePath += '.js'; + } + if (!await exists(mainFilePath)) { + fancyLog(`${ansiColors.red('Error')}: Could not find ${mainFilePath}. Use ${ansiColors.cyan('yarn watch-web')} to build the built-in extensions.`); + } } } - return { extensions, locations }; -} - -async function getBuiltInExtensionInfo(extensionPath) { - const packageJSON = await getExtensionPackageJSON(extensionPath); - if (!packageJSON) { - return undefined; - } - const builtInExtensionPath = path.basename(extensionPath); - - let children = []; - try { - children = await readdir(extensionPath); - } catch (error) { - console.log(`Can not read extension folder ${extensionPath}: ${error}`); - return; - } - const readme = children.find(child => /^readme(\.txt|\.md|)$/i.test(child)); - const changelog = children.find(child => /^changelog(\.txt|\.md|)$/i.test(child)); - const packageJSONNLS = children.find(child => /^package.nls.json$/i.test(child)); - return { - extensionPath: builtInExtensionPath, - packageJSON, - packageNLSPath: packageJSONNLS ? `${builtInExtensionPath}/${packageJSONNLS}` : undefined, - readmePath: readme ? `${builtInExtensionPath}/${readme}` : undefined, - changelogPath: changelog ? `${builtInExtensionPath}/${changelog}` : undefined - }; + return { extensions: allExtensions, locations }; } async function getDefaultExtensionInfos() { @@ -124,7 +106,7 @@ async function getDefaultExtensionInfos() { let extensionArg = args['extension']; if (!extensionArg) { - return { extensions, locations } + return { extensions, locations }; } const extensionPaths = Array.isArray(extensionArg) ? extensionArg : [extensionArg]; @@ -153,19 +135,6 @@ async function getExtensionPackageJSON(extensionPath) { return; // unsupported } - if (packageJSON.browser) { - packageJSON.main = packageJSON.browser; - - let mainFilePath = path.join(extensionPath, packageJSON.browser); - if (path.extname(mainFilePath) !== '.js') { - mainFilePath += '.js'; - } - if (!await exists(mainFilePath)) { - fancyLog(`${ansiColors.yellow('Warning')}: Could not find ${mainFilePath}. Use ${ansiColors.cyan('yarn gulp watch-web')} to build the built-in extensions.`); - } - } - packageJSON.extensionKind = ['web']; // enable for Web - const packageNLSPath = path.join(extensionPath, 'package.nls.json'); const packageNLSExists = await exists(packageNLSPath); if (packageNLSExists) { @@ -213,10 +182,6 @@ const server = http.createServer((req, res) => { // default extension requests return handleExtension(req, res, parsedUrl); } - if (/^\/builtin-extension\//.test(pathname)) { - // built-in extension requests - return handleBuiltInExtension(req, res, parsedUrl); - } if (pathname === '/') { // main web return handleRoot(req, res); @@ -253,7 +218,18 @@ server.on('error', err => { * @param {import('http').ServerResponse} res * @param {import('url').UrlWithParsedQuery} parsedUrl */ -function handleStatic(req, res, parsedUrl) { +async function handleStatic(req, res, parsedUrl) { + + if (/^\/static\/extensions\//.test(parsedUrl.pathname)) { + const relativePath = decodeURIComponent(parsedUrl.pathname.substr('/static/extensions/'.length)); + const filePath = getExtensionFilePath(relativePath, (await builtInExtensionsPromise).locations); + if (!filePath) { + return serveError(req, res, 400, `Bad request.`); + } + return serveFile(req, res, filePath, { + 'Access-Control-Allow-Origin': '*' + }); + } // Strip `/static/` from the path const relativeFilePath = path.normalize(decodeURIComponent(parsedUrl.pathname.substr('/static/'.length))); @@ -273,22 +249,9 @@ async function handleExtension(req, res, parsedUrl) { if (!filePath) { return serveError(req, res, 400, `Bad request.`); } - return serveFile(req, res, filePath); -} - -/** - * @param {import('http').IncomingMessage} req - * @param {import('http').ServerResponse} res - * @param {import('url').UrlWithParsedQuery} parsedUrl - */ -async function handleBuiltInExtension(req, res, parsedUrl) { - // Strip `/builtin-extension/` from the path - const relativePath = decodeURIComponent(parsedUrl.pathname.substr('/builtin-extension/'.length)); - const filePath = getExtensionFilePath(relativePath, (await builtInExtensionsPromise).locations); - if (!filePath) { - return serveError(req, res, 400, `Bad request.`); - } - return serveFile(req, res, filePath); + return serveFile(req, res, filePath, { + 'Access-Control-Allow-Origin': '*' + }); } /** @@ -309,7 +272,8 @@ async function handleRoot(req, res) { } const [owner, repo, ...branch] = gh.split('/', 3); - folderUri = { scheme: 'github', authority: branch.join('/') || 'HEAD', path: `/${owner}/${repo}` }; + const ref = branch.join('/'); + folderUri = { scheme: 'github', authority: `${owner}+${repo}${ref ? `+${ref}` : ''}`, path: '/' }; } else { let cs = qs.get('cs'); if (cs) { @@ -318,7 +282,8 @@ async function handleRoot(req, res) { } const [owner, repo, ...branch] = cs.split('/'); - folderUri = { scheme: 'codespace', authority: branch.join('/') || 'HEAD', path: `/${owner}/${repo}` }; + const ref = branch.join('/'); + folderUri = { scheme: 'codespace', authority: `${owner}+${repo}${ref ? `+${ref}` : ''}`, path: '/' }; } } } @@ -331,14 +296,16 @@ async function handleRoot(req, res) { fancyLog(`${ansiColors.magenta('Additional extensions')}: ${staticExtensions.map(e => path.basename(e.extensionLocation.path)).join(', ') || 'None'}`); } - const webConfigJSON = escapeAttribute(JSON.stringify({ + const webConfigJSON = { folderUri: folderUri, staticExtensions, - builtinExtensionsServiceUrl: `${SCHEME}://${AUTHORITY}/builtin-extension` - })); + }; + if (args['wrap-iframe']) { + webConfigJSON._wrapWebWorkerExtHostInIframe = true; + } const data = (await readFile(WEB_MAIN)).toString() - .replace('{{WORKBENCH_WEB_CONFIGURATION}}', () => webConfigJSON) // use a replace function to avoid that regexp replace patterns ($&, $0, ...) are applied + .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('{{WEBVIEW_ENDPOINT}}', '') .replace('{{REMOTE_USER_DATA_URI}}', ''); diff --git a/scripts/code-cli.bat b/scripts/code-cli.bat index a07ccc96bf6..2e4e34ff32e 100644 --- a/scripts/code-cli.bat +++ b/scripts/code-cli.bat @@ -5,27 +5,17 @@ title VSCode Dev pushd %~dp0\.. -:: Node modules -if not exist node_modules call yarn +:: Get electron, compile, built-in extensions +if "%VSCODE_SKIP_PRELAUNCH%"=="" node build/lib/preLaunch.js for /f "tokens=2 delims=:," %%a in ('findstr /R /C:"\"nameShort\":.*" product.json') do set NAMESHORT=%%~a set NAMESHORT=%NAMESHORT: "=% set NAMESHORT=%NAMESHORT:"=%.exe set CODE=".build\electron\%NAMESHORT%" -:: Download Electron if needed -node build\lib\electron.js -if %errorlevel% neq 0 node .\node_modules\gulp\bin\gulp.js electron - :: Manage built-in extensions if "%1"=="--builtin" goto builtin -:: Sync built-in extensions -node build\lib\builtInExtensions.js - -:: Build -if not exist out yarn compile - :: Configuration set ELECTRON_RUN_AS_NODE=1 set NODE_ENV=development diff --git a/scripts/code-cli.sh b/scripts/code-cli.sh index e4fa552e642..a792c08532e 100755 --- a/scripts/code-cli.sh +++ b/scripts/code-cli.sh @@ -18,11 +18,10 @@ function code() { CODE=".build/electron/$NAME" fi - # Node modules - test -d node_modules || yarn - - # Get electron - yarn electron + # Get electron, compile, built-in extensions + if [[ -z "${VSCODE_SKIP_PRELAUNCH}" ]]; then + node build/lib/preLaunch.js + fi # Manage built-in extensions if [[ "$1" == "--builtin" ]]; then @@ -30,12 +29,6 @@ function code() { return fi - # Sync built-in extensions - node build/lib/builtInExtensions.js - - # Build - test -d out || yarn compile - ELECTRON_RUN_AS_NODE=1 \ NODE_ENV=development \ VSCODE_DEV=1 \ diff --git a/scripts/code.bat b/scripts/code.bat index c4c1cc7c057..7ef1fd33fe0 100644 --- a/scripts/code.bat +++ b/scripts/code.bat @@ -5,26 +5,17 @@ title VSCode Dev pushd %~dp0\.. -:: Node modules -if not exist node_modules call yarn +:: Get electron, compile, built-in extensions +if "%VSCODE_SKIP_PRELAUNCH%"=="" node build/lib/preLaunch.js for /f "tokens=2 delims=:," %%a in ('findstr /R /C:"\"nameShort\":.*" product.json') do set NAMESHORT=%%~a set NAMESHORT=%NAMESHORT: "=% set NAMESHORT=%NAMESHORT:"=%.exe set CODE=".build\electron\%NAMESHORT%" -:: Get electron -call yarn electron - :: Manage built-in extensions if "%1"=="--builtin" goto builtin -:: Sync built-in extensions -node build\lib\builtInExtensions.js - -:: Build -if not exist out yarn compile - :: Configuration set NODE_ENV=development set VSCODE_DEV=1 diff --git a/scripts/code.sh b/scripts/code.sh index 390aa4b201d..b19cc0df9ff 100755 --- a/scripts/code.sh +++ b/scripts/code.sh @@ -24,11 +24,10 @@ function code() { CODE=".build/electron/$NAME" fi - # Node modules - test -d node_modules || yarn - - # Get electron - yarn electron + # Get electron, compile, built-in extensions + if [[ -z "${VSCODE_SKIP_PRELAUNCH}" ]]; then + node build/lib/preLaunch.js + fi # Manage built-in extensions if [[ "$1" == "--builtin" ]]; then @@ -36,12 +35,6 @@ function code() { return fi - # Sync built-in extensions - node build/lib/builtInExtensions.js - - # Build - test -d out || yarn compile - # Configuration export NODE_ENV=development export VSCODE_DEV=1 diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 817d95071f6..3133c7869a5 100644 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -23,6 +23,7 @@ if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( compile-extension:vscode-colorize-tests^ compile-extension:markdown-language-features^ compile-extension:typescript-language-features^ + compile-extension:vscode-custom-editor-tests^ compile-extension:vscode-notebook-tests^ compile-extension:emmet^ compile-extension:css-language-features-server^ @@ -44,9 +45,6 @@ if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( :: Tests in the extension host -call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-notebook-tests\test --enable-proposed-api=vscode.vscode-notebook-tests --extensionDevelopmentPath=%~dp0\..\extensions\vscode-notebook-tests --extensionTestsPath=%~dp0\..\extensions\vscode-notebook-tests\out --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% -if %errorlevel% neq 0 exit /b %errorlevel% - call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-api-tests\testWorkspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out\singlefolder-tests --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% if %errorlevel% neq 0 exit /b %errorlevel% @@ -65,6 +63,9 @@ if %errorlevel% neq 0 exit /b %errorlevel% call "%INTEGRATION_TEST_ELECTRON_PATH%" $%~dp0\..\extensions\emmet\out\test\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\emmet --extensionTestsPath=%~dp0\..\extensions\emmet\out\test --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% . if %errorlevel% neq 0 exit /b %errorlevel% +call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-notebook-tests\test --enable-proposed-api=vscode.vscode-notebook-tests --extensionDevelopmentPath=%~dp0\..\extensions\vscode-notebook-tests --extensionTestsPath=%~dp0\..\extensions\vscode-notebook-tests\out --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% +if %errorlevel% neq 0 exit /b %errorlevel% + for /f "delims=" %%i in ('node -p "require('fs').realpathSync.native(require('os').tmpdir())"') do set TEMPDIR=%%i set GITWORKSPACE=%TEMPDIR%\git-%RANDOM% mkdir %GITWORKSPACE% diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index 2dc5ff578d0..5412a5c0ecd 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -27,6 +27,7 @@ else # and the build bundles extensions into .build webpacked yarn gulp compile-extension:vscode-api-tests \ compile-extension:vscode-colorize-tests \ + compile-extension:vscode-custom-editor-tests \ compile-extension:vscode-notebook-tests \ compile-extension:markdown-language-features \ compile-extension:typescript-language-features \ @@ -49,7 +50,6 @@ fi ./scripts/test.sh --runGlob **/*.integrationTest.js "$@" # Tests in the extension host -"$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 "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-api-tests/testWorkspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/singlefolder-tests --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-api-tests/testworkspace.code-workspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/workspace-tests --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-colorize-tests/test --extensionDevelopmentPath=$ROOT/extensions/vscode-colorize-tests --extensionTestsPath=$ROOT/extensions/vscode-colorize-tests/out --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR @@ -57,6 +57,7 @@ fi #"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/typescript-language-features/test-workspace --extensionDevelopmentPath=$ROOT/extensions/typescript-language-features --extensionTestsPath=$ROOT/extensions/typescript-language-features/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/emmet/out/test/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/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 $(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 cd $ROOT/extensions/css-language-features/server && $ROOT/scripts/node-electron.sh test/index.js diff --git a/src/bootstrap-amd.js b/src/bootstrap-amd.js index a8144075dd5..b0b266557a6 100644 --- a/src/bootstrap-amd.js +++ b/src/bootstrap-amd.js @@ -14,7 +14,7 @@ const nlsConfig = bootstrap.setupNLS(); // Bootstrap: Loader loader.config({ - baseUrl: bootstrap.uriFromPath(__dirname), + baseUrl: bootstrap.fileUriFromPath(__dirname), catchError: true, nodeRequire: require, nodeMain: __filename, diff --git a/src/bootstrap-window.js b/src/bootstrap-window.js index a417c1deb58..9d1ddc5a957 100644 --- a/src/bootstrap-window.js +++ b/src/bootstrap-window.js @@ -21,8 +21,10 @@ globalThis.MonacoBootstrapWindow = factory(); } }(this, function () { - const path = require.__$__nodeRequire('path'); - const bootstrap = globalThis.MonacoBootstrap; + const preloadGlobals = globals(); + const sandbox = preloadGlobals.context.sandbox; + const webFrame = preloadGlobals.webFrame; + const safeProcess = sandbox ? preloadGlobals.process : process; /** * @param {string[]} modulePaths @@ -34,6 +36,7 @@ /** * // configuration: INativeWindowConfiguration * @type {{ + * zoomLevel?: number, * extensionDevelopmentPath?: string[], * extensionTestsPath?: string, * userEnv?: { [key: string]: string | undefined }, @@ -42,30 +45,40 @@ * }} */ const configuration = JSON.parse(args['config'] || '{}') || {}; + // Apply zoom level early to avoid glitches + const zoomLevel = configuration.zoomLevel; + if (typeof zoomLevel === 'number' && zoomLevel !== 0) { + webFrame.setZoomLevel(zoomLevel); + } + // Error handler - process.on('uncaughtException', function (error) { + safeProcess.on('uncaughtException', function (error) { onUnexpectedError(error, enableDeveloperTools); }); // Developer tools - const enableDeveloperTools = (process.env['VSCODE_DEV'] || !!configuration.extensionDevelopmentPath) && !configuration.extensionTestsPath; + const enableDeveloperTools = (safeProcess.env['VSCODE_DEV'] || !!configuration.extensionDevelopmentPath) && !configuration.extensionTestsPath; let developerToolsUnbind; if (enableDeveloperTools || (options && options.forceEnableDeveloperKeybindings)) { developerToolsUnbind = registerDeveloperKeybindings(options && options.disallowReloadKeybinding); } - // Correctly inherit the parent's environment - Object.assign(process.env, configuration.userEnv); + // Correctly inherit the parent's environment (TODO@sandbox non-sandboxed only) + if (!sandbox) { + Object.assign(safeProcess.env, configuration.userEnv); + } - // Enable ASAR support - bootstrap.enableASARSupport(path.join(configuration.appRoot, 'node_modules')); + // Enable ASAR support (TODO@sandbox non-sandboxed only) + if (!sandbox) { + globalThis.MonacoBootstrap.enableASARSupport(configuration.appRoot); + } if (options && typeof options.canModifyDOM === 'function') { options.canModifyDOM(configuration); } - // Get the nls configuration into the process.env as early as possible. - const nlsConfig = bootstrap.setupNLS(); + // Get the nls configuration into the process.env as early as possible (TODO@sandbox non-sandboxed only) + const nlsConfig = sandbox ? { availableLanguages: {} } : globalThis.MonacoBootstrap.setupNLS(); let locale = nlsConfig.availableLanguages['*'] || 'en'; if (locale === 'zh-tw') { @@ -76,16 +89,20 @@ window.document.documentElement.setAttribute('lang', locale); - // do not advertise AMD to avoid confusing UMD modules loaded with nodejs - window['define'] = undefined; + // do not advertise AMD to avoid confusing UMD modules loaded with nodejs (TODO@sandbox non-sandboxed only) + if (!sandbox) { + window['define'] = undefined; + } - // replace the patched electron fs with the original node fs for all AMD code - require.define('fs', ['original-fs'], function (originalFS) { return originalFS; }); + // replace the patched electron fs with the original node fs for all AMD code (TODO@sandbox non-sandboxed only) + if (!sandbox) { + require.define('fs', ['original-fs'], function (originalFS) { return originalFS; }); + } window['MonacoEnvironment'] = {}; const loaderConfig = { - baseUrl: `${bootstrap.uriFromPath(configuration.appRoot)}/out`, + baseUrl: `${uriFromPath(configuration.appRoot)}/out`, 'vs/nls': nlsConfig, amdModulesPattern: /^vs\//, }; @@ -150,7 +167,7 @@ * @returns {() => void} */ function registerDeveloperKeybindings(disallowReloadKeybinding) { - const ipcRenderer = globals().ipcRenderer; + const ipcRenderer = preloadGlobals.ipcRenderer; const extractKey = function (e) { return [ @@ -163,9 +180,9 @@ }; // Devtools & reload support - const TOGGLE_DEV_TOOLS_KB = (process.platform === 'darwin' ? 'meta-alt-73' : 'ctrl-shift-73'); // mac: Cmd-Alt-I, rest: Ctrl-Shift-I + const TOGGLE_DEV_TOOLS_KB = (safeProcess.platform === 'darwin' ? 'meta-alt-73' : 'ctrl-shift-73'); // mac: Cmd-Alt-I, rest: Ctrl-Shift-I const TOGGLE_DEV_TOOLS_KB_ALT = '123'; // F12 - const RELOAD_KB = (process.platform === 'darwin' ? 'meta-82' : 'ctrl-82'); // mac: Cmd-R, rest: Ctrl-R + const RELOAD_KB = (safeProcess.platform === 'darwin' ? 'meta-82' : 'ctrl-82'); // mac: Cmd-R, rest: Ctrl-R let listener = function (e) { const key = extractKey(e); @@ -192,7 +209,7 @@ */ function onUnexpectedError(error, enableDeveloperTools) { if (enableDeveloperTools) { - const ipcRenderer = globals().ipcRenderer; + const ipcRenderer = preloadGlobals.ipcRenderer; ipcRenderer.send('vscode:openDevTools'); } @@ -211,6 +228,31 @@ return window.vscode; } + /** + * TODO@sandbox this should not use the file:// protocol at all + * and be consolidated with the fileUriFromPath() method in + * bootstrap.js. + * + * @param {string} path + * @returns {string} + */ + function uriFromPath(path) { + let pathName = path.replace(/\\/g, '/'); + if (pathName.length > 0 && pathName.charAt(0) !== '/') { + pathName = `/${pathName}`; + } + + /** @type {string} */ + let uri; + if (safeProcess.platform === 'win32' && pathName.startsWith('//')) { // specially handle Windows UNC paths + uri = encodeURI(`file:${pathName}`); + } else { + uri = encodeURI(`file://${pathName}`); + } + + return uri.replace(/#/g, '%23'); + } + return { load, globals diff --git a/src/bootstrap.js b/src/bootstrap.js index f1f579c7b7c..d1abd5502df 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -16,7 +16,11 @@ // Browser else { - globalThis.MonacoBootstrap = factory(); + try { + globalThis.MonacoBootstrap = factory(); + } catch (error) { + console.warn(error); // expected when e.g. running with sandbox: true (TODO@sandbox eventually consolidate this) + } } }(this, function () { const Module = require('module'); @@ -40,10 +44,10 @@ //#region Add support for using node_modules.asar /** - * @param {string=} nodeModulesPath + * @param {string} appRoot */ - function enableASARSupport(nodeModulesPath) { - let NODE_MODULES_PATH = nodeModulesPath; + function enableASARSupport(appRoot) { + let NODE_MODULES_PATH = appRoot ? path.join(appRoot, 'node_modules') : undefined; if (!NODE_MODULES_PATH) { NODE_MODULES_PATH = path.join(__dirname, '../node_modules'); } else { @@ -83,7 +87,7 @@ * @param {string} _path * @returns {string} */ - function uriFromPath(_path) { + function fileUriFromPath(_path) { let pathName = path.resolve(_path).replace(/\\/g, '/'); if (pathName.length > 0 && pathName.charAt(0) !== '/') { pathName = `/${pathName}`; @@ -132,7 +136,7 @@ } const bundleFile = path.join(nlsConfig._resolvedLanguagePackCoreLocation, `${bundle.replace(/\//g, '!')}.nls.json`); - readFile(bundleFile).then(function (content) { + fs.promises.readFile(bundleFile, 'utf8').then(function (content) { const json = JSON.parse(content); bundles[bundle] = json; @@ -140,7 +144,7 @@ }).catch((error) => { try { if (nlsConfig._corruptedFile) { - writeFile(nlsConfig._corruptedFile, 'corrupted').catch(function (error) { console.error(error); }); + fs.promises.writeFile(nlsConfig._corruptedFile, 'corrupted', 'utf8').catch(function (error) { console.error(error); }); } } finally { cb(error, undefined); @@ -152,23 +156,6 @@ return nlsConfig; } - /** - * @param {string} file - * @returns {Promise} - */ - function readFile(file) { - return fs.promises.readFile(file, 'utf8'); - } - - /** - * @param {string} file - * @param {string} content - * @returns {Promise} - */ - function writeFile(file, content) { - return fs.promises.writeFile(file, content, 'utf8'); - } - //#endregion @@ -254,6 +241,6 @@ avoidMonkeyPatchFromAppInsights, configurePortable, setupNLS, - uriFromPath + fileUriFromPath }; })); diff --git a/src/typings/node.processEnv-ext.d.ts b/src/typings/node.processEnv-ext.d.ts index fec557ff2a7..4ca44ec5b37 100644 --- a/src/typings/node.processEnv-ext.d.ts +++ b/src/typings/node.processEnv-ext.d.ts @@ -8,7 +8,7 @@ declare namespace NodeJS { export interface Process { /** - * The lazy enviroment is a promise that resolves to `process.env` + * The lazy environment 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 diff --git a/src/vs/base/browser/contextmenu.ts b/src/vs/base/browser/contextmenu.ts index 6a5d3f79d2b..2119700f859 100644 --- a/src/vs/base/browser/contextmenu.ts +++ b/src/vs/base/browser/contextmenu.ts @@ -3,10 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IAction, IActionRunner } from 'vs/base/common/actions'; -import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IAction, IActionRunner, IActionViewItem } from 'vs/base/common/actions'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; -import { SubmenuAction } from 'vs/base/browser/ui/menu/menu'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; export interface IContextMenuEvent { @@ -16,15 +14,9 @@ export interface IContextMenuEvent { readonly metaKey?: boolean; } -export class ContextSubMenu extends SubmenuAction { - constructor(label: string, public entries: Array) { - super(label, entries, 'contextsubmenu'); - } -} - export interface IContextMenuDelegate { getAnchor(): HTMLElement | { x: number; y: number; width?: number; height?: number; }; - getActions(): ReadonlyArray; + getActions(): IAction[]; getCheckedActionsRepresentation?(action: IAction): 'radio' | 'checkbox'; getActionViewItem?(action: IAction): IActionViewItem | undefined; getActionsContext?(event?: IContextMenuEvent): any; @@ -34,5 +26,9 @@ export interface IContextMenuDelegate { actionRunner?: IActionRunner; autoSelectFirstItem?: boolean; anchorAlignment?: AnchorAlignment; - anchorAsContainer?: boolean; + domForShadowRoot?: HTMLElement; +} + +export interface IContextMenuProvider { + showContextMenu(delegate: IContextMenuDelegate): void; } diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index d5622349ff0..f203119f8f6 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -749,6 +749,16 @@ export function getShadowRoot(domNode: Node): ShadowRoot | null { return isShadowRoot(domNode) ? domNode : null; } +export function getActiveElement(): Element | null { + let result = document.activeElement; + + while (result?.shadowRoot) { + result = result.shadowRoot.activeElement; + } + + return result; +} + export function createStyleSheet(container: HTMLElement = document.getElementsByTagName('head')[0]): HTMLStyleElement { let style = document.createElement('style'); style.type = 'text/css'; diff --git a/src/vs/base/browser/ui/actionbar/actionViewItems.ts b/src/vs/base/browser/ui/actionbar/actionViewItems.ts new file mode 100644 index 00000000000..b7916bf107d --- /dev/null +++ b/src/vs/base/browser/ui/actionbar/actionViewItems.ts @@ -0,0 +1,398 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./actionbar'; +import * as platform from 'vs/base/common/platform'; +import * as nls from 'vs/nls'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { SelectBox, ISelectOptionItem, ISelectBoxOptions } from 'vs/base/browser/ui/selectBox/selectBox'; +import { IAction, IActionRunner, Action, IActionChangeEvent, ActionRunner, Separator, IActionViewItem } from 'vs/base/common/actions'; +import * as DOM from 'vs/base/browser/dom'; +import * as types from 'vs/base/common/types'; +import { EventType, Gesture } from 'vs/base/browser/touch'; +import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { DataTransfers } from 'vs/base/browser/dnd'; +import { isFirefox } from 'vs/base/browser/browser'; + +export interface IBaseActionViewItemOptions { + draggable?: boolean; + isMenu?: boolean; + useEventAsContext?: boolean; +} + +export class BaseActionViewItem extends Disposable implements IActionViewItem { + + element: HTMLElement | undefined; + + _context: any; + _action: IAction; + + private _actionRunner: IActionRunner | undefined; + + constructor(context: any, action: IAction, protected options: IBaseActionViewItemOptions = {}) { + super(); + + this._context = context || this; + this._action = action; + + if (action instanceof Action) { + this._register(action.onDidChange(event => { + if (!this.element) { + // we have not been rendered yet, so there + // is no point in updating the UI + return; + } + + this.handleActionChangeEvent(event); + })); + } + } + + private handleActionChangeEvent(event: IActionChangeEvent): void { + if (event.enabled !== undefined) { + this.updateEnabled(); + } + + if (event.checked !== undefined) { + this.updateChecked(); + } + + if (event.class !== undefined) { + this.updateClass(); + } + + if (event.label !== undefined) { + this.updateLabel(); + this.updateTooltip(); + } + + if (event.tooltip !== undefined) { + this.updateTooltip(); + } + } + + get actionRunner(): IActionRunner { + if (!this._actionRunner) { + this._actionRunner = this._register(new ActionRunner()); + } + + return this._actionRunner; + } + + set actionRunner(actionRunner: IActionRunner) { + this._actionRunner = actionRunner; + } + + getAction(): IAction { + return this._action; + } + + isEnabled(): boolean { + return this._action.enabled; + } + + setActionContext(newContext: unknown): void { + this._context = newContext; + } + + render(container: HTMLElement): void { + const element = this.element = container; + this._register(Gesture.addTarget(container)); + + const enableDragging = this.options && this.options.draggable; + if (enableDragging) { + container.draggable = true; + + if (isFirefox) { + // Firefox: requires to set a text data transfer to get going + this._register(DOM.addDisposableListener(container, DOM.EventType.DRAG_START, e => e.dataTransfer?.setData(DataTransfers.TEXT, this._action.label))); + } + } + + this._register(DOM.addDisposableListener(element, EventType.Tap, e => this.onClick(e))); + + this._register(DOM.addDisposableListener(element, DOM.EventType.MOUSE_DOWN, e => { + if (!enableDragging) { + DOM.EventHelper.stop(e, true); // do not run when dragging is on because that would disable it + } + + if (this._action.enabled && e.button === 0) { + DOM.addClass(element, 'active'); + } + })); + + if (platform.isMacintosh) { + // macOS: allow to trigger the button when holding Ctrl+key and pressing the + // main mouse button. This is for scenarios where e.g. some interaction forces + // the Ctrl+key to be pressed and hold but the user still wants to interact + // with the actions (for example quick access in quick navigation mode). + this._register(DOM.addDisposableListener(element, DOM.EventType.CONTEXT_MENU, e => { + if (e.button === 0 && e.ctrlKey === true) { + this.onClick(e); + } + })); + } + + this._register(DOM.addDisposableListener(element, DOM.EventType.CLICK, e => { + DOM.EventHelper.stop(e, true); + + // menus do not use the click event + if (!(this.options && this.options.isMenu)) { + platform.setImmediate(() => this.onClick(e)); + } + })); + + this._register(DOM.addDisposableListener(element, DOM.EventType.DBLCLICK, e => { + DOM.EventHelper.stop(e, true); + })); + + [DOM.EventType.MOUSE_UP, DOM.EventType.MOUSE_OUT].forEach(event => { + this._register(DOM.addDisposableListener(element, event, e => { + DOM.EventHelper.stop(e); + DOM.removeClass(element, 'active'); + })); + }); + } + + onClick(event: DOM.EventLike): void { + DOM.EventHelper.stop(event, true); + + const context = types.isUndefinedOrNull(this._context) ? this.options?.useEventAsContext ? event : undefined : this._context; + this.actionRunner.run(this._action, context); + } + + focus(): void { + if (this.element) { + this.element.focus(); + DOM.addClass(this.element, 'focused'); + } + } + + blur(): void { + if (this.element) { + this.element.blur(); + DOM.removeClass(this.element, 'focused'); + } + } + + protected updateEnabled(): void { + // implement in subclass + } + + protected updateLabel(): void { + // implement in subclass + } + + protected updateTooltip(): void { + // implement in subclass + } + + protected updateClass(): void { + // implement in subclass + } + + protected updateChecked(): void { + // implement in subclass + } + + dispose(): void { + if (this.element) { + DOM.removeNode(this.element); + this.element = undefined; + } + + super.dispose(); + } +} + +export interface IActionViewItemOptions extends IBaseActionViewItemOptions { + icon?: boolean; + label?: boolean; + keybinding?: string | null; +} + +export class ActionViewItem extends BaseActionViewItem { + + protected label: HTMLElement | undefined; + protected options: IActionViewItemOptions; + + private cssClass?: string; + + constructor(context: unknown, action: IAction, options: IActionViewItemOptions = {}) { + super(context, action, options); + + this.options = options; + this.options.icon = options.icon !== undefined ? options.icon : false; + this.options.label = options.label !== undefined ? options.label : true; + this.cssClass = ''; + } + + render(container: HTMLElement): void { + super.render(container); + + if (this.element) { + this.label = DOM.append(this.element, DOM.$('a.action-label')); + } + + if (this.label) { + if (this._action.id === Separator.ID) { + this.label.setAttribute('role', 'presentation'); // A separator is a presentation item + } else { + if (this.options.isMenu) { + this.label.setAttribute('role', 'menuitem'); + } else { + this.label.setAttribute('role', 'button'); + } + } + } + + if (this.options.label && this.options.keybinding && this.element) { + DOM.append(this.element, DOM.$('span.keybinding')).textContent = this.options.keybinding; + } + + this.updateClass(); + this.updateLabel(); + this.updateTooltip(); + this.updateEnabled(); + this.updateChecked(); + } + + focus(): void { + super.focus(); + + if (this.label) { + this.label.focus(); + } + } + + updateLabel(): void { + if (this.options.label && this.label) { + this.label.textContent = this.getAction().label; + } + } + + updateTooltip(): void { + let title: string | null = null; + + if (this.getAction().tooltip) { + title = this.getAction().tooltip; + + } else if (!this.options.label && this.getAction().label && this.options.icon) { + title = this.getAction().label; + + if (this.options.keybinding) { + title = nls.localize({ key: 'titleLabel', comment: ['action title', 'action keybinding'] }, "{0} ({1})", title, this.options.keybinding); + } + } + + if (title && this.label) { + this.label.title = title; + } + } + + updateClass(): void { + if (this.cssClass && this.label) { + DOM.removeClasses(this.label, this.cssClass); + } + + if (this.options.icon) { + this.cssClass = this.getAction().class; + + if (this.label) { + DOM.addClass(this.label, 'codicon'); + if (this.cssClass) { + DOM.addClasses(this.label, this.cssClass); + } + } + + this.updateEnabled(); + } else { + if (this.label) { + DOM.removeClass(this.label, 'codicon'); + } + } + } + + updateEnabled(): void { + if (this.getAction().enabled) { + if (this.label) { + this.label.removeAttribute('aria-disabled'); + DOM.removeClass(this.label, 'disabled'); + this.label.tabIndex = 0; + } + + if (this.element) { + DOM.removeClass(this.element, 'disabled'); + } + } else { + if (this.label) { + this.label.setAttribute('aria-disabled', 'true'); + DOM.addClass(this.label, 'disabled'); + DOM.removeTabIndexAndUpdateFocus(this.label); + } + + if (this.element) { + DOM.addClass(this.element, 'disabled'); + } + } + } + + updateChecked(): void { + if (this.label) { + if (this.getAction().checked) { + DOM.addClass(this.label, 'checked'); + } else { + DOM.removeClass(this.label, 'checked'); + } + } + } +} + +export class SelectActionViewItem extends BaseActionViewItem { + protected selectBox: SelectBox; + + constructor(ctx: unknown, action: IAction, options: ISelectOptionItem[], selected: number, contextViewProvider: IContextViewProvider, selectBoxOptions?: ISelectBoxOptions) { + super(ctx, action); + + this.selectBox = new SelectBox(options, selected, contextViewProvider, undefined, selectBoxOptions); + + this._register(this.selectBox); + this.registerListeners(); + } + + setOptions(options: ISelectOptionItem[], selected?: number): void { + this.selectBox.setOptions(options, selected); + } + + select(index: number): void { + this.selectBox.select(index); + } + + private registerListeners(): void { + this._register(this.selectBox.onDidSelect(e => { + this.actionRunner.run(this._action, this.getActionContext(e.selected, e.index)); + })); + } + + protected getActionContext(option: string, index: number) { + return option; + } + + focus(): void { + if (this.selectBox) { + this.selectBox.focus(); + } + } + + blur(): void { + if (this.selectBox) { + this.selectBox.blur(); + } + } + + render(container: HTMLElement): void { + this.selectBox.render(container); + } +} diff --git a/src/vs/base/browser/ui/actionbar/actionbar.css b/src/vs/base/browser/ui/actionbar/actionbar.css index 9b304e81a80..79c2a927201 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.css +++ b/src/vs/base/browser/ui/actionbar/actionbar.css @@ -5,7 +5,6 @@ .monaco-action-bar { text-align: right; - overflow: hidden; white-space: nowrap; } diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index 2a5ec66eb5c..bbc0918db24 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -4,382 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./actionbar'; -import * as platform from 'vs/base/common/platform'; -import * as nls from 'vs/nls'; -import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { SelectBox, ISelectOptionItem, ISelectBoxOptions } from 'vs/base/browser/ui/selectBox/selectBox'; -import { IAction, IActionRunner, Action, IActionChangeEvent, ActionRunner, IRunEvent } from 'vs/base/common/actions'; +import { Disposable, dispose } from 'vs/base/common/lifecycle'; +import { IAction, IActionRunner, ActionRunner, IRunEvent, Separator, IActionViewItem, IActionViewItemProvider } from 'vs/base/common/actions'; import * as DOM from 'vs/base/browser/dom'; import * as types from 'vs/base/common/types'; -import { EventType, Gesture } from 'vs/base/browser/touch'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; import { Event, Emitter } from 'vs/base/common/event'; -import { DataTransfers } from 'vs/base/browser/dnd'; -import { isFirefox } from 'vs/base/browser/browser'; - -export interface IActionViewItem extends IDisposable { - actionRunner: IActionRunner; - setActionContext(context: any): void; - render(element: HTMLElement): void; - isEnabled(): boolean; - focus(fromRight?: boolean): void; - blur(): void; -} - -export interface IBaseActionViewItemOptions { - draggable?: boolean; - isMenu?: boolean; - useEventAsContext?: boolean; -} - -export class BaseActionViewItem extends Disposable implements IActionViewItem { - - element: HTMLElement | undefined; - - _context: any; - _action: IAction; - - private _actionRunner: IActionRunner | undefined; - - constructor(context: any, action: IAction, protected options?: IBaseActionViewItemOptions) { - super(); - - this._context = context || this; - this._action = action; - - if (action instanceof Action) { - this._register(action.onDidChange(event => { - if (!this.element) { - // we have not been rendered yet, so there - // is no point in updating the UI - return; - } - - this.handleActionChangeEvent(event); - })); - } - } - - private handleActionChangeEvent(event: IActionChangeEvent): void { - if (event.enabled !== undefined) { - this.updateEnabled(); - } - - if (event.checked !== undefined) { - this.updateChecked(); - } - - if (event.class !== undefined) { - this.updateClass(); - } - - if (event.label !== undefined) { - this.updateLabel(); - this.updateTooltip(); - } - - if (event.tooltip !== undefined) { - this.updateTooltip(); - } - } - - get actionRunner(): IActionRunner { - if (!this._actionRunner) { - this._actionRunner = this._register(new ActionRunner()); - } - - return this._actionRunner; - } - - set actionRunner(actionRunner: IActionRunner) { - this._actionRunner = actionRunner; - } - - getAction(): IAction { - return this._action; - } - - isEnabled(): boolean { - return this._action.enabled; - } - - setActionContext(newContext: unknown): void { - this._context = newContext; - } - - render(container: HTMLElement): void { - const element = this.element = container; - this._register(Gesture.addTarget(container)); - - const enableDragging = this.options && this.options.draggable; - if (enableDragging) { - container.draggable = true; - - if (isFirefox) { - // Firefox: requires to set a text data transfer to get going - this._register(DOM.addDisposableListener(container, DOM.EventType.DRAG_START, e => e.dataTransfer?.setData(DataTransfers.TEXT, this._action.label))); - } - } - - this._register(DOM.addDisposableListener(element, EventType.Tap, e => this.onClick(e))); - - this._register(DOM.addDisposableListener(element, DOM.EventType.MOUSE_DOWN, e => { - if (!enableDragging) { - DOM.EventHelper.stop(e, true); // do not run when dragging is on because that would disable it - } - - if (this._action.enabled && e.button === 0) { - DOM.addClass(element, 'active'); - } - })); - - if (platform.isMacintosh) { - // macOS: allow to trigger the button when holding Ctrl+key and pressing the - // main mouse button. This is for scenarios where e.g. some interaction forces - // the Ctrl+key to be pressed and hold but the user still wants to interact - // with the actions (for example quick access in quick navigation mode). - this._register(DOM.addDisposableListener(element, DOM.EventType.CONTEXT_MENU, e => { - if (e.button === 0 && e.ctrlKey === true) { - this.onClick(e); - } - })); - } - - this._register(DOM.addDisposableListener(element, DOM.EventType.CLICK, e => { - DOM.EventHelper.stop(e, true); - // See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard - // > Writing to the clipboard - // > You can use the "cut" and "copy" commands without any special - // permission if you are using them in a short-lived event handler - // for a user action (for example, a click handler). - - // => to get the Copy and Paste context menu actions working on Firefox, - // there should be no timeout here - if (this.options && this.options.isMenu) { - this.onClick(e); - } else { - platform.setImmediate(() => this.onClick(e)); - } - })); - - this._register(DOM.addDisposableListener(element, DOM.EventType.DBLCLICK, e => { - DOM.EventHelper.stop(e, true); - })); - - [DOM.EventType.MOUSE_UP, DOM.EventType.MOUSE_OUT].forEach(event => { - this._register(DOM.addDisposableListener(element, event, e => { - DOM.EventHelper.stop(e); - DOM.removeClass(element, 'active'); - })); - }); - } - - onClick(event: DOM.EventLike): void { - DOM.EventHelper.stop(event, true); - - const context = types.isUndefinedOrNull(this._context) ? this.options?.useEventAsContext ? event : undefined : this._context; - this.actionRunner.run(this._action, context); - } - - focus(): void { - if (this.element) { - this.element.focus(); - DOM.addClass(this.element, 'focused'); - } - } - - blur(): void { - if (this.element) { - this.element.blur(); - DOM.removeClass(this.element, 'focused'); - } - } - - protected updateEnabled(): void { - // implement in subclass - } - - protected updateLabel(): void { - // implement in subclass - } - - protected updateTooltip(): void { - // implement in subclass - } - - protected updateClass(): void { - // implement in subclass - } - - protected updateChecked(): void { - // implement in subclass - } - - dispose(): void { - if (this.element) { - DOM.removeNode(this.element); - this.element = undefined; - } - - super.dispose(); - } -} - -export class Separator extends Action { - - static readonly ID = 'vs.actions.separator'; - - constructor(label?: string) { - super(Separator.ID, label, label ? 'separator text' : 'separator'); - this.checked = false; - this.enabled = false; - } -} - -export interface IActionViewItemOptions extends IBaseActionViewItemOptions { - icon?: boolean; - label?: boolean; - keybinding?: string | null; -} - -export class ActionViewItem extends BaseActionViewItem { - - protected label: HTMLElement | undefined; - protected options: IActionViewItemOptions; - - private cssClass?: string; - - constructor(context: unknown, action: IAction, options: IActionViewItemOptions = {}) { - super(context, action, options); - - this.options = options; - this.options.icon = options.icon !== undefined ? options.icon : false; - this.options.label = options.label !== undefined ? options.label : true; - this.cssClass = ''; - } - - render(container: HTMLElement): void { - super.render(container); - - if (this.element) { - this.label = DOM.append(this.element, DOM.$('a.action-label')); - } - - if (this.label) { - if (this._action.id === Separator.ID) { - this.label.setAttribute('role', 'presentation'); // A separator is a presentation item - } else { - if (this.options.isMenu) { - this.label.setAttribute('role', 'menuitem'); - } else { - this.label.setAttribute('role', 'button'); - } - } - } - - if (this.options.label && this.options.keybinding && this.element) { - DOM.append(this.element, DOM.$('span.keybinding')).textContent = this.options.keybinding; - } - - this.updateClass(); - this.updateLabel(); - this.updateTooltip(); - this.updateEnabled(); - this.updateChecked(); - } - - focus(): void { - super.focus(); - - if (this.label) { - this.label.focus(); - } - } - - updateLabel(): void { - if (this.options.label && this.label) { - this.label.textContent = this.getAction().label; - } - } - - updateTooltip(): void { - let title: string | null = null; - - if (this.getAction().tooltip) { - title = this.getAction().tooltip; - - } else if (!this.options.label && this.getAction().label && this.options.icon) { - title = this.getAction().label; - - if (this.options.keybinding) { - title = nls.localize({ key: 'titleLabel', comment: ['action title', 'action keybinding'] }, "{0} ({1})", title, this.options.keybinding); - } - } - - if (title && this.label) { - this.label.title = title; - } - } - - updateClass(): void { - if (this.cssClass && this.label) { - DOM.removeClasses(this.label, this.cssClass); - } - - if (this.options.icon) { - this.cssClass = this.getAction().class; - - if (this.label) { - DOM.addClass(this.label, 'codicon'); - if (this.cssClass) { - DOM.addClasses(this.label, this.cssClass); - } - } - - this.updateEnabled(); - } else { - if (this.label) { - DOM.removeClass(this.label, 'codicon'); - } - } - } - - updateEnabled(): void { - if (this.getAction().enabled) { - if (this.label) { - this.label.removeAttribute('aria-disabled'); - DOM.removeClass(this.label, 'disabled'); - this.label.tabIndex = 0; - } - - if (this.element) { - DOM.removeClass(this.element, 'disabled'); - } - } else { - if (this.label) { - this.label.setAttribute('aria-disabled', 'true'); - DOM.addClass(this.label, 'disabled'); - DOM.removeTabIndexAndUpdateFocus(this.label); - } - - if (this.element) { - DOM.addClass(this.element, 'disabled'); - } - } - } - - updateChecked(): void { - if (this.label) { - if (this.getAction().checked) { - DOM.addClass(this.label, 'checked'); - } else { - DOM.removeClass(this.label, 'checked'); - } - } - } -} +import { IActionViewItemOptions, ActionViewItem, BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; export const enum ActionsOrientation { HORIZONTAL, @@ -393,41 +25,30 @@ export interface ActionTrigger { keyDown: boolean; } -export interface IActionViewItemProvider { - (action: IAction): IActionViewItem | undefined; -} - export interface IActionBarOptions { - orientation?: ActionsOrientation; - context?: any; - actionViewItemProvider?: IActionViewItemProvider; - actionRunner?: IActionRunner; - ariaLabel?: string; - animated?: boolean; - triggerKeys?: ActionTrigger; - allowContextMenu?: boolean; - preventLoopNavigation?: boolean; + readonly orientation?: ActionsOrientation; + readonly context?: any; + readonly actionViewItemProvider?: IActionViewItemProvider; + readonly actionRunner?: IActionRunner; + readonly ariaLabel?: string; + readonly animated?: boolean; + readonly triggerKeys?: ActionTrigger; + readonly allowContextMenu?: boolean; + readonly preventLoopNavigation?: boolean; } -const defaultOptions: IActionBarOptions = { - orientation: ActionsOrientation.HORIZONTAL, - context: null, - triggerKeys: { - keys: [KeyCode.Enter, KeyCode.Space], - keyDown: false - } -}; - export interface IActionOptions extends IActionViewItemOptions { index?: number; } export class ActionBar extends Disposable implements IActionRunner { - options: IActionBarOptions; + private readonly options: IActionBarOptions; private _actionRunner: IActionRunner; private _context: unknown; + private _orientation: ActionsOrientation; + private _triggerKeys: ActionTrigger; // View Items viewItems: IActionViewItem[]; @@ -450,15 +71,16 @@ export class ActionBar extends Disposable implements IActionRunner { private _onDidBeforeRun = this._register(new Emitter()); readonly onDidBeforeRun: Event = this._onDidBeforeRun.event; - constructor(container: HTMLElement, options: IActionBarOptions = defaultOptions) { + constructor(container: HTMLElement, options: IActionBarOptions = {}) { super(); this.options = options; - this._context = options.context; - - if (!this.options.triggerKeys) { - this.options.triggerKeys = defaultOptions.triggerKeys; - } + this._context = options.context ?? null; + this._orientation = this.options.orientation ?? ActionsOrientation.HORIZONTAL; + this._triggerKeys = this.options.triggerKeys ?? { + keys: [KeyCode.Enter, KeyCode.Space], + keyDown: false + }; if (this.options.actionRunner) { this._actionRunner = this.options.actionRunner; @@ -483,7 +105,7 @@ export class ActionBar extends Disposable implements IActionRunner { let previousKey: KeyCode; let nextKey: KeyCode; - switch (this.options.orientation) { + switch (this._orientation) { case ActionsOrientation.HORIZONTAL: previousKey = KeyCode.LeftArrow; nextKey = KeyCode.RightArrow; @@ -517,7 +139,7 @@ export class ActionBar extends Disposable implements IActionRunner { this._onDidCancel.fire(); } else if (this.isTriggerKeyEvent(event)) { // Staying out of the else branch even if not triggered - if (this.options.triggerKeys && this.options.triggerKeys.keyDown) { + if (this._triggerKeys.keyDown) { this.doTrigger(event); } } else { @@ -535,7 +157,7 @@ export class ActionBar extends Disposable implements IActionRunner { // Run action on Enter/Space if (this.isTriggerKeyEvent(event)) { - if (this.options.triggerKeys && !this.options.triggerKeys.keyDown) { + if (!this._triggerKeys.keyDown) { this.doTrigger(event); } @@ -551,7 +173,7 @@ export class ActionBar extends Disposable implements IActionRunner { this.focusTracker = this._register(DOM.trackFocus(this.domNode)); this._register(this.focusTracker.onDidBlur(() => { - if (document.activeElement === this.domNode || !DOM.isAncestor(document.activeElement, this.domNode)) { + if (DOM.getActiveElement() === this.domNode || !DOM.isAncestor(DOM.getActiveElement(), this.domNode)) { this._onDidBlur.fire(); this.focusedItem = undefined; } @@ -582,11 +204,9 @@ export class ActionBar extends Disposable implements IActionRunner { private isTriggerKeyEvent(event: StandardKeyboardEvent): boolean { let ret = false; - if (this.options.triggerKeys) { - this.options.triggerKeys.keys.forEach(keyCode => { - ret = ret || event.equals(keyCode); - }); - } + this._triggerKeys.keys.forEach(keyCode => { + ret = ret || event.equals(keyCode); + }); return ret; } @@ -594,7 +214,7 @@ export class ActionBar extends Disposable implements IActionRunner { private updateFocusedItem(): void { for (let i = 0; i < this.actionsList.children.length; i++) { const elem = this.actionsList.children[i]; - if (DOM.isAncestor(document.activeElement, elem)) { + if (DOM.isAncestor(DOM.getActiveElement(), elem)) { this.focusedItem = i; break; } @@ -845,53 +465,6 @@ export class ActionBar extends Disposable implements IActionRunner { } } -export class SelectActionViewItem extends BaseActionViewItem { - protected selectBox: SelectBox; - - constructor(ctx: unknown, action: IAction, options: ISelectOptionItem[], selected: number, contextViewProvider: IContextViewProvider, selectBoxOptions?: ISelectBoxOptions) { - super(ctx, action); - - this.selectBox = new SelectBox(options, selected, contextViewProvider, undefined, selectBoxOptions); - - this._register(this.selectBox); - this.registerListeners(); - } - - setOptions(options: ISelectOptionItem[], selected?: number): void { - this.selectBox.setOptions(options, selected); - } - - select(index: number): void { - this.selectBox.select(index); - } - - private registerListeners(): void { - this._register(this.selectBox.onDidSelect(e => { - this.actionRunner.run(this._action, this.getActionContext(e.selected, e.index)); - })); - } - - protected getActionContext(option: string, index: number) { - return option; - } - - focus(): void { - if (this.selectBox) { - this.selectBox.focus(); - } - } - - blur(): void { - if (this.selectBox) { - this.selectBox.blur(); - } - } - - render(container: HTMLElement): void { - this.selectBox.render(container); - } -} - export function prepareActions(actions: IAction[]): IAction[] { if (!actions.length) { return actions; diff --git a/src/vs/base/browser/ui/checkbox/checkbox.ts b/src/vs/base/browser/ui/checkbox/checkbox.ts index 867f2f4d4b0..e29d71d60b1 100644 --- a/src/vs/base/browser/ui/checkbox/checkbox.ts +++ b/src/vs/base/browser/ui/checkbox/checkbox.ts @@ -10,9 +10,9 @@ import { Widget } from 'vs/base/browser/ui/widget'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Codicon } from 'vs/base/common/codicons'; +import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; export interface ICheckboxOpts extends ICheckboxStyles { readonly actionClassName?: string; diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 733d36ca30d..82acc8995b8 100644 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/src/vs/base/browser/ui/codicons/codiconStyles.ts b/src/vs/base/browser/ui/codicons/codiconStyles.ts index 899af845e8f..b3dc12fb3f3 100644 --- a/src/vs/base/browser/ui/codicons/codiconStyles.ts +++ b/src/vs/base/browser/ui/codicons/codiconStyles.ts @@ -28,7 +28,7 @@ function initialize() { delayer.schedule(); } -function formatRule(c: Codicon) { +export function formatRule(c: Codicon) { let def = c.definition; while (def instanceof Codicon) { def = def.definition; diff --git a/src/vs/base/browser/ui/contextview/contextview.ts b/src/vs/base/browser/ui/contextview/contextview.ts index 93ecd03dfec..60a97219140 100644 --- a/src/vs/base/browser/ui/contextview/contextview.ts +++ b/src/vs/base/browser/ui/contextview/contextview.ts @@ -10,6 +10,12 @@ import { IDisposable, toDisposable, Disposable, DisposableStore } from 'vs/base/ import { Range } from 'vs/base/common/range'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; +export const enum ContextViewDOMPosition { + ABSOLUTE = 1, + FIXED, + FIXED_SHADOW +} + export interface IAnchor { x: number; y: number; @@ -105,32 +111,62 @@ export class ContextView extends Disposable { private container: HTMLElement | null = null; private view: HTMLElement; private useFixedPosition: boolean; + private useShadowDOM: boolean; private delegate: IDelegate | null = null; private toDisposeOnClean: IDisposable = Disposable.None; private toDisposeOnSetContainer: IDisposable = Disposable.None; + private shadowRoot: ShadowRoot | null = null; + private shadowRootHostElement: HTMLElement | null = null; - constructor(container: HTMLElement, useFixedPosition: boolean) { + constructor(container: HTMLElement, domPosition: ContextViewDOMPosition) { super(); this.view = DOM.$('.context-view'); this.useFixedPosition = false; + this.useShadowDOM = false; DOM.hide(this.view); - this.setContainer(container, useFixedPosition); + this.setContainer(container, domPosition); - this._register(toDisposable(() => this.setContainer(null, false))); + this._register(toDisposable(() => this.setContainer(null, ContextViewDOMPosition.ABSOLUTE))); } - setContainer(container: HTMLElement | null, useFixedPosition: boolean): void { + setContainer(container: HTMLElement | null, domPosition: ContextViewDOMPosition): void { if (this.container) { this.toDisposeOnSetContainer.dispose(); - this.container.removeChild(this.view); + + if (this.shadowRoot) { + this.shadowRoot.removeChild(this.view); + this.shadowRoot = null; + DOM.removeNode(this.shadowRootHostElement!); + this.shadowRootHostElement = null; + } else { + this.container.removeChild(this.view); + } + this.container = null; } if (container) { this.container = container; - this.container.appendChild(this.view); + + this.useFixedPosition = domPosition !== ContextViewDOMPosition.ABSOLUTE; + this.useShadowDOM = domPosition === ContextViewDOMPosition.FIXED_SHADOW; + + if (this.useShadowDOM) { + this.shadowRootHostElement = DOM.$('.shadow-root-host'); + this.container.appendChild(this.shadowRootHostElement); + this.shadowRoot = this.shadowRootHostElement.attachShadow({ mode: 'open' }); + this.shadowRoot.innerHTML = ` + + `; + this.shadowRoot.appendChild(this.view); + this.shadowRoot.appendChild(DOM.$('slot')); + } else { + this.container.appendChild(this.view); + } const toDisposeOnSetContainer = new DisposableStore(); @@ -148,8 +184,6 @@ export class ContextView extends Disposable { this.toDisposeOnSetContainer = toDisposeOnSetContainer; } - - this.useFixedPosition = useFixedPosition; } show(delegate: IDelegate): void { @@ -162,6 +196,7 @@ export class ContextView extends Disposable { this.view.className = 'context-view'; this.view.style.top = '0px'; this.view.style.left = '0px'; + this.view.style.zIndex = '2500'; this.view.style.position = this.useFixedPosition ? 'fixed' : 'absolute'; DOM.show(this.view); @@ -180,6 +215,10 @@ export class ContextView extends Disposable { } } + getViewElement(): HTMLElement { + return this.view; + } + layout(): void { if (!this.isVisible()) { return; @@ -300,3 +339,45 @@ export class ContextView extends Disposable { super.dispose(); } } + +let SHADOW_ROOT_CSS = /* css */ ` + :host { + all: initial; /* 1st rule so subsequent properties are reset. */ + } + + @font-face { + font-family: "codicon"; + src: url("./codicon.ttf?5d4d76ab2ce5108968ad644d591a16a6") format("truetype"); + } + + .codicon[class*='codicon-'] { + font: normal normal normal 16px/1 codicon; + display: inline-block; + text-decoration: none; + text-rendering: auto; + text-align: center; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + } + + :host-context(.mac) { font-family: -apple-system, BlinkMacSystemFont, sans-serif; } + :host-context(.mac:lang(zh-Hans)) { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; } + :host-context(.mac:lang(zh-Hant)) { font-family: -apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif; } + :host-context(.mac:lang(ja)) { font-family: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic Pro", sans-serif; } + :host-context(.mac:lang(ko)) { font-family: -apple-system, BlinkMacSystemFont, "Nanum Gothic", "Apple SD Gothic Neo", "AppleGothic", sans-serif; } + + :host-context(.windows) { font-family: "Segoe WPC", "Segoe UI", sans-serif; } + :host-context(.windows:lang(zh-Hans)) { font-family: "Segoe WPC", "Segoe UI", "Microsoft YaHei", sans-serif; } + :host-context(.windows:lang(zh-Hant)) { font-family: "Segoe WPC", "Segoe UI", "Microsoft Jhenghei", sans-serif; } + :host-context(.windows:lang(ja)) { font-family: "Segoe WPC", "Segoe UI", "Yu Gothic UI", "Meiryo UI", sans-serif; } + :host-context(.windows:lang(ko)) { font-family: "Segoe WPC", "Segoe UI", "Malgun Gothic", "Dotom", sans-serif; } + + :host-context(.linux) { font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; } + :host-context(.linux:lang(zh-Hans)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; } + :host-context(.linux:lang(zh-Hant)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; } + :host-context(.linux:lang(ja)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; } + :host-context(.linux:lang(ko)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; } +`; diff --git a/src/vs/base/browser/ui/dropdown/dropdown.ts b/src/vs/base/browser/ui/dropdown/dropdown.ts index 24d879aac6f..b74cab78bb6 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.ts +++ b/src/vs/base/browser/ui/dropdown/dropdown.ts @@ -5,14 +5,13 @@ import 'vs/css!./dropdown'; import { Gesture, EventType as GestureEventType } from 'vs/base/browser/touch'; -import { ActionRunner, IAction, IActionRunner } from 'vs/base/common/actions'; -import { BaseActionViewItem, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionRunner, IAction } from 'vs/base/common/actions'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IContextViewProvider, IAnchor, AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { IMenuOptions } from 'vs/base/browser/ui/menu/menu'; -import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes'; -import { EventHelper, EventType, removeClass, addClass, append, $, addDisposableListener, addClasses, DOMEvent } from 'vs/base/browser/dom'; -import { IContextMenuDelegate } from 'vs/base/browser/contextmenu'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { EventHelper, EventType, removeClass, addClass, append, $, addDisposableListener, DOMEvent } from 'vs/base/browser/dom'; +import { IContextMenuProvider } from 'vs/base/browser/contextmenu'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Emitter } from 'vs/base/common/event'; @@ -201,17 +200,13 @@ export class Dropdown extends BaseDropdown { } } -export interface IContextMenuProvider { - showContextMenu(delegate: IContextMenuDelegate): void; -} - export interface IActionProvider { - getActions(): ReadonlyArray; + getActions(): IAction[]; } export interface IDropdownMenuOptions extends IBaseDropdownOptions { contextMenuProvider: IContextMenuProvider; - actions?: ReadonlyArray; + actions?: IAction[]; actionProvider?: IActionProvider; menuClassName?: string; menuAsChild?: boolean; // scope down for #99448 @@ -220,7 +215,7 @@ export interface IDropdownMenuOptions extends IBaseDropdownOptions { export class DropdownMenu extends BaseDropdown { private _contextMenuProvider: IContextMenuProvider; private _menuOptions: IMenuOptions | undefined; - private _actions: ReadonlyArray = []; + private _actions: IAction[] = []; private actionProvider?: IActionProvider; private menuClassName: string; private menuAsChild?: boolean; @@ -243,7 +238,7 @@ export class DropdownMenu extends BaseDropdown { return this._menuOptions; } - private get actions(): ReadonlyArray { + private get actions(): IAction[] { if (this.actionProvider) { return this.actionProvider.getActions(); } @@ -251,7 +246,7 @@ export class DropdownMenu extends BaseDropdown { return this._actions; } - private set actions(actions: ReadonlyArray) { + private set actions(actions: IAction[]) { this._actions = actions; } @@ -270,7 +265,7 @@ export class DropdownMenu extends BaseDropdown { onHide: () => this.onHide(), actionRunner: this.menuOptions ? this.menuOptions.actionRunner : undefined, anchorAlignment: this.menuOptions ? this.menuOptions.anchorAlignment : AnchorAlignment.LEFT, - anchorAsContainer: this.menuAsChild + domForShadowRoot: this.menuAsChild ? this.element : undefined }); } @@ -283,104 +278,3 @@ export class DropdownMenu extends BaseDropdown { removeClass(this.element, 'active'); } } - -export class DropdownMenuActionViewItem extends BaseActionViewItem { - private menuActionsOrProvider: ReadonlyArray | IActionProvider; - private dropdownMenu: DropdownMenu | undefined; - private contextMenuProvider: IContextMenuProvider; - private actionViewItemProvider?: IActionViewItemProvider; - private keybindings?: (action: IAction) => ResolvedKeybinding | undefined; - private clazz: string | undefined; - private anchorAlignmentProvider: (() => AnchorAlignment) | undefined; - private menuAsChild?: boolean; - - private _onDidChangeVisibility = this._register(new Emitter()); - readonly onDidChangeVisibility = this._onDidChangeVisibility.event; - - constructor(action: IAction, menuActions: ReadonlyArray, contextMenuProvider: IContextMenuProvider, actionViewItemProvider: IActionViewItemProvider | undefined, actionRunner: IActionRunner, keybindings: ((action: IAction) => ResolvedKeybinding | undefined) | undefined, clazz: string | undefined, anchorAlignmentProvider?: () => AnchorAlignment, menuAsChild?: boolean); - constructor(action: IAction, actionProvider: IActionProvider, contextMenuProvider: IContextMenuProvider, actionViewItemProvider: IActionViewItemProvider | undefined, actionRunner: IActionRunner, keybindings: ((action: IAction) => ResolvedKeybinding) | undefined, clazz: string | undefined, anchorAlignmentProvider?: () => AnchorAlignment, menuAsChild?: boolean); - constructor(action: IAction, menuActionsOrProvider: ReadonlyArray | IActionProvider, contextMenuProvider: IContextMenuProvider, actionViewItemProvider: IActionViewItemProvider | undefined, actionRunner: IActionRunner, keybindings: ((action: IAction) => ResolvedKeybinding | undefined) | undefined, clazz: string | undefined, anchorAlignmentProvider?: () => AnchorAlignment, menuAsChild?: boolean) { - super(null, action); - - this.menuActionsOrProvider = menuActionsOrProvider; - this.contextMenuProvider = contextMenuProvider; - this.actionViewItemProvider = actionViewItemProvider; - this.actionRunner = actionRunner; - this.keybindings = keybindings; - this.clazz = clazz; - this.anchorAlignmentProvider = anchorAlignmentProvider; - this.menuAsChild = menuAsChild; - } - - render(container: HTMLElement): void { - const labelRenderer: ILabelRenderer = (el: HTMLElement): IDisposable | null => { - this.element = append(el, $('a.action-label.codicon')); // todo@aeschli: remove codicon, should come through `this.clazz` - if (this.clazz) { - addClasses(this.element, this.clazz); - } - - this.element.tabIndex = 0; - this.element.setAttribute('role', 'button'); - this.element.setAttribute('aria-haspopup', 'true'); - this.element.setAttribute('aria-expanded', 'false'); - this.element.title = this._action.label || ''; - - return null; - }; - - const options: IDropdownMenuOptions = { - contextMenuProvider: this.contextMenuProvider, - labelRenderer: labelRenderer, - menuAsChild: this.menuAsChild - }; - - // Render the DropdownMenu around a simple action to toggle it - if (Array.isArray(this.menuActionsOrProvider)) { - options.actions = this.menuActionsOrProvider; - } else { - options.actionProvider = this.menuActionsOrProvider as IActionProvider; - } - - this.dropdownMenu = this._register(new DropdownMenu(container, options)); - this._register(this.dropdownMenu.onDidChangeVisibility(visible => { - this.element?.setAttribute('aria-expanded', `${visible}`); - this._onDidChangeVisibility.fire(visible); - })); - - this.dropdownMenu.menuOptions = { - actionViewItemProvider: this.actionViewItemProvider, - actionRunner: this.actionRunner, - getKeyBinding: this.keybindings, - context: this._context - }; - - if (this.anchorAlignmentProvider) { - const that = this; - - this.dropdownMenu.menuOptions = { - ...this.dropdownMenu.menuOptions, - get anchorAlignment(): AnchorAlignment { - return that.anchorAlignmentProvider!(); - } - }; - } - } - - setActionContext(newContext: unknown): void { - super.setActionContext(newContext); - - if (this.dropdownMenu) { - if (this.dropdownMenu.menuOptions) { - this.dropdownMenu.menuOptions.context = newContext; - } else { - this.dropdownMenu.menuOptions = { context: newContext }; - } - } - } - - show(): void { - if (this.dropdownMenu) { - this.dropdownMenu.show(); - } - } -} diff --git a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts new file mode 100644 index 00000000000..db045b26c13 --- /dev/null +++ b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./dropdown'; +import { IAction, IActionRunner, IActionViewItemProvider } from 'vs/base/common/actions'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; +import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; +import { append, $, addClasses } from 'vs/base/browser/dom'; +import { Emitter } from 'vs/base/common/event'; +import { BaseActionViewItem, IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { IActionProvider, DropdownMenu, IDropdownMenuOptions, ILabelRenderer } from 'vs/base/browser/ui/dropdown/dropdown'; +import { IContextMenuProvider } from 'vs/base/browser/contextmenu'; +import { asArray } from 'vs/base/common/arrays'; + +export interface IKeybindingProvider { + (action: IAction): ResolvedKeybinding | undefined; +} + +export interface IAnchorAlignmentProvider { + (): AnchorAlignment; +} + +export interface IDropdownMenuActionViewItemOptions extends IBaseActionViewItemOptions { + readonly actionViewItemProvider?: IActionViewItemProvider; + readonly keybindingProvider?: IKeybindingProvider; + readonly actionRunner?: IActionRunner; + readonly classNames?: string[] | string; + readonly anchorAlignmentProvider?: IAnchorAlignmentProvider; + readonly menuAsChild?: boolean; +} + +export class DropdownMenuActionViewItem extends BaseActionViewItem { + private menuActionsOrProvider: readonly IAction[] | IActionProvider; + private dropdownMenu: DropdownMenu | undefined; + private contextMenuProvider: IContextMenuProvider; + + private _onDidChangeVisibility = this._register(new Emitter()); + readonly onDidChangeVisibility = this._onDidChangeVisibility.event; + + constructor( + action: IAction, + menuActionsOrProvider: readonly IAction[] | IActionProvider, + contextMenuProvider: IContextMenuProvider, + protected options: IDropdownMenuActionViewItemOptions = {} + ) { + super(null, action, options); + + this.menuActionsOrProvider = menuActionsOrProvider; + this.contextMenuProvider = contextMenuProvider; + + if (this.options.actionRunner) { + this.actionRunner = this.options.actionRunner; + } + } + + render(container: HTMLElement): void { + const labelRenderer: ILabelRenderer = (el: HTMLElement): IDisposable | null => { + this.element = append(el, $('a.action-label')); + + const classNames = this.options.classNames ? asArray(this.options.classNames) : []; + + // todo@aeschli: remove codicon, should come through `this.options.classNames` + if (!classNames.find(c => c === 'icon')) { + classNames.push('codicon'); + } + + addClasses(this.element, ...classNames); + + this.element.tabIndex = 0; + this.element.setAttribute('role', 'button'); + this.element.setAttribute('aria-haspopup', 'true'); + this.element.setAttribute('aria-expanded', 'false'); + this.element.title = this._action.label || ''; + + return null; + }; + + const options: IDropdownMenuOptions = { + contextMenuProvider: this.contextMenuProvider, + labelRenderer: labelRenderer, + menuAsChild: this.options.menuAsChild + }; + + // Render the DropdownMenu around a simple action to toggle it + if (Array.isArray(this.menuActionsOrProvider)) { + options.actions = this.menuActionsOrProvider; + } else { + options.actionProvider = this.menuActionsOrProvider as IActionProvider; + } + + this.dropdownMenu = this._register(new DropdownMenu(container, options)); + this._register(this.dropdownMenu.onDidChangeVisibility(visible => { + this.element?.setAttribute('aria-expanded', `${visible}`); + this._onDidChangeVisibility.fire(visible); + })); + + this.dropdownMenu.menuOptions = { + actionViewItemProvider: this.options.actionViewItemProvider, + actionRunner: this.actionRunner, + getKeyBinding: this.options.keybindingProvider, + context: this._context + }; + + if (this.options.anchorAlignmentProvider) { + const that = this; + + this.dropdownMenu.menuOptions = { + ...this.dropdownMenu.menuOptions, + get anchorAlignment(): AnchorAlignment { + return that.options.anchorAlignmentProvider!(); + } + }; + } + } + + setActionContext(newContext: unknown): void { + super.setActionContext(newContext); + + if (this.dropdownMenu) { + if (this.dropdownMenu.menuOptions) { + this.dropdownMenu.menuOptions.context = newContext; + } else { + this.dropdownMenu.menuOptions = { context: newContext }; + } + } + } + + show(): void { + if (this.dropdownMenu) { + this.dropdownMenu.show(); + } + } +} diff --git a/src/vs/base/browser/ui/menu/menu.css b/src/vs/base/browser/ui/menu/menu.css deleted file mode 100644 index d86ee055d51..00000000000 --- a/src/vs/base/browser/ui/menu/menu.css +++ /dev/null @@ -1,225 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-menu .monaco-action-bar.vertical { - margin-left: 0; - overflow: visible; -} - -.monaco-menu .monaco-action-bar.vertical .actions-container { - display: block; -} - -.monaco-menu .monaco-action-bar.vertical .action-item { - padding: 0; - transform: none; - display: flex; -} - -.monaco-menu .monaco-action-bar.vertical .action-item.active { - transform: none; -} - -.monaco-menu .monaco-action-bar.vertical .action-menu-item { - flex: 1 1 auto; - display: flex; - height: 2em; - align-items: center; - position: relative; -} - -.monaco-menu .monaco-action-bar.vertical .action-label { - flex: 1 1 auto; - text-decoration: none; - padding: 0 1em; - background: none; - font-size: 12px; - line-height: 1; -} - -.monaco-menu .monaco-action-bar.vertical .keybinding, -.monaco-menu .monaco-action-bar.vertical .submenu-indicator { - display: inline-block; - flex: 2 1 auto; - padding: 0 1em; - text-align: right; - font-size: 12px; - line-height: 1; -} - -.monaco-menu .monaco-action-bar.vertical .submenu-indicator { - height: 100%; -} - -.monaco-menu .monaco-action-bar.vertical .submenu-indicator.codicon { - font-size: 16px !important; - display: flex; - align-items: center; -} - -.monaco-menu .monaco-action-bar.vertical .submenu-indicator.codicon::before { - margin-left: auto; - margin-right: -20px; -} - -.monaco-menu .monaco-action-bar.vertical .action-item.disabled .keybinding, -.monaco-menu .monaco-action-bar.vertical .action-item.disabled .submenu-indicator { - opacity: 0.4; -} - -.monaco-menu .monaco-action-bar.vertical .action-label:not(.separator) { - display: inline-block; - box-sizing: border-box; - margin: 0; -} - -.monaco-menu .monaco-action-bar.vertical .action-item { - position: static; - overflow: visible; -} - -.monaco-menu .monaco-action-bar.vertical .action-item .monaco-submenu { - position: absolute; -} - -.monaco-menu .monaco-action-bar.vertical .action-label.separator { - padding: 0.5em 0 0 0; - margin-bottom: 0.5em; - width: 100%; - height: 0px !important; - margin-left: .8em !important; - margin-right: .8em !important; -} - -.monaco-menu .monaco-action-bar.vertical .action-label.separator.text { - padding: 0.7em 1em 0.1em 1em; - font-weight: bold; - opacity: 1; -} - -.monaco-menu .monaco-action-bar.vertical .action-label:hover { - color: inherit; -} - -.monaco-menu .monaco-action-bar.vertical .menu-item-check { - position: absolute; - visibility: hidden; - width: 1em; - height: 100%; -} - -.monaco-menu .monaco-action-bar.vertical .action-menu-item.checked .menu-item-check { - visibility: visible; - display: flex; - align-items: center; - justify-content: center; -} - -/* Context Menu */ - -.context-view.monaco-menu-container { - outline: 0; - border: none; - animation: fadeIn 0.083s linear; -} - -.context-view.monaco-menu-container :focus, -.context-view.monaco-menu-container .monaco-action-bar.vertical:focus, -.context-view.monaco-menu-container .monaco-action-bar.vertical :focus { - outline: 0; -} - -.monaco-menu .monaco-action-bar.vertical .action-item { - border: thin solid transparent; /* prevents jumping behaviour on hover or focus */ -} - - -/* High Contrast Theming */ -.hc-black .context-view.monaco-menu-container { - box-shadow: none; -} - -.hc-black .monaco-menu .monaco-action-bar.vertical .action-item.focused { - background: none; -} - -/* Menubar styles */ - -.menubar { - display: flex; - flex-shrink: 1; - box-sizing: border-box; - height: 30px; - overflow: hidden; - flex-wrap: wrap; -} - -.fullscreen .menubar:not(.compact) { - margin: 0px; - padding: 0px 5px; -} - -.menubar > .menubar-menu-button { - align-items: center; - box-sizing: border-box; - padding: 0px 8px; - cursor: default; - -webkit-app-region: no-drag; - zoom: 1; - white-space: nowrap; - outline: 0; -} - -.menubar.compact { - flex-shrink: 0; - overflow: visible; /* to avoid the compact menu to be repositioned when clicking */ -} - -.menubar.compact > .menubar-menu-button { - width: 100%; - height: 100%; - padding: 0px; -} - -.menubar .menubar-menu-items-holder { - position: absolute; - left: 0px; - opacity: 1; - z-index: 2000; -} - -.menubar .menubar-menu-items-holder.monaco-menu-container { - outline: 0; - border: none; -} - -.menubar .menubar-menu-items-holder.monaco-menu-container :focus { - outline: 0; -} - -.menubar .toolbar-toggle-more { - width: 20px; - height: 100%; -} - -.menubar.compact .toolbar-toggle-more { - position: relative; - left: 0px; - top: 0px; - cursor: pointer; - width: 100%; - display: flex; - align-items: center; - justify-content: center; -} - -.menubar .toolbar-toggle-more { - padding: 0; - vertical-align: sub; -} - -.menubar.compact .toolbar-toggle-more::before { - content: "\eb94" !important; -} diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index 69dff14fb41..028c8c157be 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -3,13 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./menu'; import * as nls from 'vs/nls'; import * as strings from 'vs/base/common/strings'; -import { IActionRunner, IAction, Action } from 'vs/base/common/actions'; -import { ActionBar, IActionViewItemProvider, ActionsOrientation, Separator, ActionViewItem, IActionViewItemOptions, BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IActionRunner, IAction, SubmenuAction, Separator, IActionViewItemProvider } from 'vs/base/common/actions'; +import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes'; -import { addClass, EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor, hasClass, addDisposableListener, removeClass, append, $, addClasses, removeClasses, clearNode } from 'vs/base/browser/dom'; +import { addClass, EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor, hasClass, addDisposableListener, removeClass, append, $, addClasses, removeClasses, clearNode, createStyleSheet, isInShadowDOM, getActiveElement, Dimension, IDomNodePagePosition } from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { RunOnceScheduler } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; @@ -17,9 +16,13 @@ import { Color } from 'vs/base/common/color'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable'; import { Event } from 'vs/base/common/event'; -import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; +import { AnchorAlignment, layout, LayoutAnchorPosition } from 'vs/base/browser/ui/contextview/contextview'; import { isLinux, isMacintosh } from 'vs/base/common/platform'; import { Codicon, registerIcon, stripCodicons } from 'vs/base/common/codicons'; +import { BaseActionViewItem, ActionViewItem, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { formatRule } from 'vs/base/browser/ui/codicons/codiconStyles'; +import { isFirefox } from 'vs/base/browser/browser'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; export const MENU_MNEMONIC_REGEX = /\(&([^\s&])\)|(^|[^&])&([^\s&])/; export const MENU_ESCAPED_MNEMONIC_REGEX = /(&)?(&)([^\s&])/g; @@ -42,6 +45,7 @@ export interface IMenuOptions { anchorAlignment?: AnchorAlignment; expandDirection?: Direction; useEventAsContext?: boolean; + submenuIds?: Set; } export interface IMenuStyles { @@ -55,12 +59,6 @@ export interface IMenuStyles { separatorColor?: Color; } -export class SubmenuAction extends Action { - constructor(label: string, public entries: ReadonlyArray, cssClass?: string) { - super(!!cssClass ? cssClass : 'submenu', label, '', true); - } -} - interface ISubMenuData { parent: Menu; submenu?: Menu; @@ -71,6 +69,8 @@ export class Menu extends ActionBar { private readonly menuDisposables: DisposableStore; private scrollableElement: DomScrollableElement; private menuElement: HTMLElement; + static globalStyleSheet: HTMLStyleElement; + protected styleSheet: HTMLStyleElement | undefined; constructor(container: HTMLElement, actions: ReadonlyArray, options: IMenuOptions = {}) { addClass(container, 'monaco-menu-container'); @@ -96,6 +96,8 @@ export class Menu extends ActionBar { this.menuDisposables = this._register(new DisposableStore()); + this.initializeStyleSheet(container); + addDisposableListener(menuElement, EventType.KEY_DOWN, (e) => { const event = new StandardKeyboardEvent(e); @@ -203,7 +205,16 @@ export class Menu extends ActionBar { e.preventDefault(); })); - menuElement.style.maxHeight = `${Math.max(10, window.innerHeight - container.getBoundingClientRect().top - 30)}px`; + menuElement.style.maxHeight = `${Math.max(10, window.innerHeight - container.getBoundingClientRect().top - 35)}px`; + + actions = actions.filter(a => { + if (options.submenuIds?.has(a.id)) { + console.warn(`Found submenu cycle: ${a.id}`); + return false; + } + + return true; + }); this.push(actions, { icon: true, label: true, isMenu: true }); @@ -215,6 +226,20 @@ export class Menu extends ActionBar { }); } + private initializeStyleSheet(container: HTMLElement): void { + if (isInShadowDOM(container)) { + this.styleSheet = createStyleSheet(container); + this.styleSheet.innerHTML = MENU_WIDGET_CSS; + } else { + if (!Menu.globalStyleSheet) { + Menu.globalStyleSheet = createStyleSheet(); + Menu.globalStyleSheet.innerHTML = MENU_WIDGET_CSS; + } + + this.styleSheet = Menu.globalStyleSheet; + } + } + style(style: IMenuStyles): void { const container = this.getContainer(); @@ -299,7 +324,8 @@ export class Menu extends ActionBar { if (action instanceof Separator) { return new MenuSeparatorActionViewItem(options.context, action, { icon: true }); } else if (action instanceof SubmenuAction) { - const menuActionViewItem = new SubmenuMenuActionViewItem(action, action.entries, parentData, options); + 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]) }); if (options.enableMnemonics) { const mnemonic = menuActionViewItem.getMnemonic(); @@ -399,7 +425,37 @@ class BaseMenuActionViewItem extends BaseActionViewItem { // with BaseActionViewItem #101537 // add back if issues arise and link new issue EventHelper.stop(e, true); - this.onClick(e); + + // See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard + // > Writing to the clipboard + // > You can use the "cut" and "copy" commands without any special + // permission if you are using them in a short-lived event handler + // for a user action (for example, a click handler). + + // => to get the Copy and Paste context menu actions working on Firefox, + // there should be no timeout here + if (isFirefox) { + const mouseEvent = new StandardMouseEvent(e); + + // Allowing right click to trigger the event causes the issue described below, + // but since the solution below does not work in FF, we must disable right click + if (mouseEvent.rightButton) { + return; + } + + this.onClick(e); + } + + // In all other cases, set timout to allow context menu cancellation to trigger + // otherwise the action will destroy the menu and a second context menu + // will still trigger for right click. + setTimeout(() => { + this.onClick(e); + }, 0); + })); + + this._register(addDisposableListener(this.element, EventType.CONTEXT_MENU, e => { + EventHelper.stop(e, true); })); }, 100); @@ -655,7 +711,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem { }, 250); this.hideScheduler = new RunOnceScheduler(() => { - if (this.element && (!isAncestor(document.activeElement, this.element) && this.parentData.submenu === this.mysubmenu)) { + if (this.element && (!isAncestor(getActiveElement(), this.element) && this.parentData.submenu === this.mysubmenu)) { this.parentData.parent.focus(false); this.cleanupExistingSubmenu(true); } @@ -689,7 +745,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem { this._register(addDisposableListener(this.element, EventType.KEY_DOWN, e => { let event = new StandardKeyboardEvent(e); - if (document.activeElement === this.item) { + if (getActiveElement() === this.item) { if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Enter)) { EventHelper.stop(e, true); } @@ -709,7 +765,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem { })); this._register(addDisposableListener(this.element, EventType.FOCUS_OUT, e => { - if (this.element && !isAncestor(document.activeElement, this.element)) { + if (this.element && !isAncestor(getActiveElement(), this.element)) { this.hideScheduler.schedule(); } })); @@ -745,6 +801,33 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem { } } + private calculateSubmenuMenuLayout(windowDimensions: Dimension, submenu: Dimension, entry: IDomNodePagePosition, expandDirection: Direction): { top: number, left: number } { + const ret = { top: 0, left: 0 }; + + // Start with horizontal + ret.left = layout(windowDimensions.width, submenu.width, { position: expandDirection === Direction.Right ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, offset: entry.left, size: entry.width }); + + // We don't have enough room to layout the menu fully, so we are overlapping the menu + if (ret.left >= entry.left && ret.left < entry.left + entry.width) { + if (entry.left + 10 + submenu.width <= windowDimensions.width) { + ret.left = entry.left + 10; + } + + entry.top += 10; + entry.height = 0; + } + + // Now that we have a horizontal position, try layout vertically + ret.top = layout(windowDimensions.height, submenu.height, { position: LayoutAnchorPosition.Before, offset: entry.top, size: 0 }); + + // We didn't have enough room below, but we did above, so we shift down to align the menu + if (ret.top + submenu.height === entry.top && ret.top + entry.height + submenu.height <= windowDimensions.height) { + ret.top += entry.height; + } + + return ret; + } + private createSubmenu(selectFirstItem = true): void { if (!this.element) { return; @@ -759,29 +842,31 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem { // This allows the menu constructor to calculate the proper max height const computedStyles = getComputedStyle(this.parentData.parent.domNode); const paddingTop = parseFloat(computedStyles.paddingTop || '0') || 0; - this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset - paddingTop}px`; + // this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset - paddingTop}px`; + this.submenuContainer.style.zIndex = '1'; + this.submenuContainer.style.position = 'fixed'; + this.submenuContainer.style.top = '0'; + this.submenuContainer.style.left = '0'; this.parentData.submenu = new Menu(this.submenuContainer, this.submenuActions, this.submenuOptions); if (this.menuStyle) { this.parentData.submenu.style(this.menuStyle); } - const boundingRect = this.element.getBoundingClientRect(); - const childBoundingRect = this.submenuContainer.getBoundingClientRect(); + // layout submenu + const entryBox = this.element.getBoundingClientRect(); + const entryBoxUpdated = { + top: entryBox.top - paddingTop, + left: entryBox.left, + height: entryBox.height + 2 * paddingTop, + width: entryBox.width + }; - if (this.expandDirection === Direction.Right) { - if (window.innerWidth <= boundingRect.right + childBoundingRect.width) { - this.submenuContainer.style.left = '10px'; - this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset + boundingRect.height}px`; - } else { - this.submenuContainer.style.left = `${this.element.offsetWidth}px`; - this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset - paddingTop}px`; - } - } else if (this.expandDirection === Direction.Left) { - this.submenuContainer.style.right = `${this.element.offsetWidth}px`; - this.submenuContainer.style.left = 'auto'; - this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset - paddingTop}px`; - } + const viewBox = this.submenuContainer.getBoundingClientRect(); + + const { top, left } = this.calculateSubmenuMenuLayout({ height: window.innerHeight, width: window.innerWidth }, viewBox, entryBoxUpdated, this.expandDirection); + this.submenuContainer.style.left = `${left}px`; + this.submenuContainer.style.top = `${top}px`; this.submenuDisposables.add(addDisposableListener(this.submenuContainer, EventType.KEY_UP, e => { let event = new StandardKeyboardEvent(e); @@ -877,3 +962,400 @@ export function cleanMnemonic(label: string): string { return label.replace(regex, mnemonicInText ? '$2$3' : '').trim(); } + +let MENU_WIDGET_CSS: string = /* css */` +.monaco-menu { + font-size: 13px; + +} + +${formatRule(menuSelectionIcon)} +${formatRule(menuSubmenuIcon)} + +.monaco-menu .monaco-action-bar { + text-align: right; + overflow: hidden; + white-space: nowrap; +} + +.monaco-menu .monaco-action-bar .actions-container { + display: flex; + margin: 0 auto; + padding: 0; + width: 100%; + justify-content: flex-end; +} + +.monaco-menu .monaco-action-bar.vertical .actions-container { + display: inline-block; +} + +.monaco-menu .monaco-action-bar.reverse .actions-container { + flex-direction: row-reverse; +} + +.monaco-menu .monaco-action-bar .action-item { + cursor: pointer; + display: inline-block; + transition: transform 50ms ease; + position: relative; /* DO NOT REMOVE - this is the key to preventing the ghosting icon bug in Chrome 42 */ +} + +.monaco-menu .monaco-action-bar .action-item.disabled { + cursor: default; +} + +.monaco-menu .monaco-action-bar.animated .action-item.active { + transform: scale(1.272019649, 1.272019649); /* 1.272019649 = āˆšĻ† */ +} + +.monaco-menu .monaco-action-bar .action-item .icon, +.monaco-menu .monaco-action-bar .action-item .codicon { + display: inline-block; +} + +.monaco-menu .monaco-action-bar .action-item .codicon { + display: flex; + align-items: center; +} + +.monaco-menu .monaco-action-bar .action-label { + font-size: 11px; + margin-right: 4px; +} + +.monaco-menu .monaco-action-bar .action-item.disabled .action-label, +.monaco-menu .monaco-action-bar .action-item.disabled .action-label:hover { + opacity: 0.4; +} + +/* Vertical actions */ + +.monaco-menu .monaco-action-bar.vertical { + text-align: left; +} + +.monaco-menu .monaco-action-bar.vertical .action-item { + display: block; +} + +.monaco-menu .monaco-action-bar.vertical .action-label.separator { + display: block; + border-bottom: 1px solid #bbb; + padding-top: 1px; + margin-left: .8em; + margin-right: .8em; +} + +.monaco-menu .secondary-actions .monaco-action-bar .action-label { + margin-left: 6px; +} + +/* Action Items */ +.monaco-menu .monaco-action-bar .action-item.select-container { + overflow: hidden; /* somehow the dropdown overflows its container, we prevent it here to not push */ + flex: 1; + max-width: 170px; + min-width: 60px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 10px; +} + +.monaco-menu .monaco-action-bar.vertical { + margin-left: 0; + overflow: visible; +} + +.monaco-menu .monaco-action-bar.vertical .actions-container { + display: block; +} + +.monaco-menu .monaco-action-bar.vertical .action-item { + padding: 0; + transform: none; + display: flex; +} + +.monaco-menu .monaco-action-bar.vertical .action-item.active { + transform: none; +} + +.monaco-menu .monaco-action-bar.vertical .action-menu-item { + flex: 1 1 auto; + display: flex; + height: 2em; + align-items: center; + position: relative; +} + +.monaco-menu .monaco-action-bar.vertical .action-label { + flex: 1 1 auto; + text-decoration: none; + padding: 0 1em; + background: none; + font-size: 12px; + line-height: 1; +} + +.monaco-menu .monaco-action-bar.vertical .keybinding, +.monaco-menu .monaco-action-bar.vertical .submenu-indicator { + display: inline-block; + flex: 2 1 auto; + padding: 0 1em; + text-align: right; + font-size: 12px; + line-height: 1; +} + +.monaco-menu .monaco-action-bar.vertical .submenu-indicator { + height: 100%; +} + +.monaco-menu .monaco-action-bar.vertical .submenu-indicator.codicon { + font-size: 16px !important; + display: flex; + align-items: center; +} + +.monaco-menu .monaco-action-bar.vertical .submenu-indicator.codicon::before { + margin-left: auto; + margin-right: -20px; +} + +.monaco-menu .monaco-action-bar.vertical .action-item.disabled .keybinding, +.monaco-menu .monaco-action-bar.vertical .action-item.disabled .submenu-indicator { + opacity: 0.4; +} + +.monaco-menu .monaco-action-bar.vertical .action-label:not(.separator) { + display: inline-block; + box-sizing: border-box; + margin: 0; +} + +.monaco-menu .monaco-action-bar.vertical .action-item { + position: static; + overflow: visible; +} + +.monaco-menu .monaco-action-bar.vertical .action-item .monaco-submenu { + position: absolute; +} + +.monaco-menu .monaco-action-bar.vertical .action-label.separator { + padding: 0.5em 0 0 0; + margin-bottom: 0.5em; + width: 100%; + height: 0px !important; + margin-left: .8em !important; + margin-right: .8em !important; +} + +.monaco-menu .monaco-action-bar.vertical .action-label.separator.text { + padding: 0.7em 1em 0.1em 1em; + font-weight: bold; + opacity: 1; +} + +.monaco-menu .monaco-action-bar.vertical .action-label:hover { + color: inherit; +} + +.monaco-menu .monaco-action-bar.vertical .menu-item-check { + position: absolute; + visibility: hidden; + width: 1em; + height: 100%; +} + +.monaco-menu .monaco-action-bar.vertical .action-menu-item.checked .menu-item-check { + visibility: visible; + display: flex; + align-items: center; + justify-content: center; +} + +/* Context Menu */ + +.context-view.monaco-menu-container { + outline: 0; + border: none; + animation: fadeIn 0.083s linear; +} + +.context-view.monaco-menu-container :focus, +.context-view.monaco-menu-container .monaco-action-bar.vertical:focus, +.context-view.monaco-menu-container .monaco-action-bar.vertical :focus { + outline: 0; +} + +.monaco-menu .monaco-action-bar.vertical .action-item { + border: thin solid transparent; /* prevents jumping behaviour on hover or focus */ +} + + +/* High Contrast Theming */ +:host-context(.hc-black) .context-view.monaco-menu-container { + box-shadow: none; +} + +:host-context(.hc-black) .monaco-menu .monaco-action-bar.vertical .action-item.focused { + background: none; +} + +/* Vertical Action Bar Styles */ + +.monaco-menu .monaco-action-bar.vertical { + padding: .5em 0; +} + +.monaco-menu .monaco-action-bar.vertical .action-menu-item { + height: 1.8em; +} + +.monaco-menu .monaco-action-bar.vertical .action-label:not(.separator), +.monaco-menu .monaco-action-bar.vertical .keybinding { + font-size: inherit; + padding: 0 2em; +} + +.monaco-menu .monaco-action-bar.vertical .menu-item-check { + font-size: inherit; + width: 2em; +} + +.monaco-menu .monaco-action-bar.vertical .action-label.separator { + font-size: inherit; + padding: 0.2em 0 0 0; + margin-bottom: 0.2em; +} + +:host-context(.linux) .monaco-menu .monaco-action-bar.vertical .action-label.separator { + margin-left: 0; + margin-right: 0; +} + +.monaco-menu .monaco-action-bar.vertical .submenu-indicator { + font-size: 60%; + padding: 0 1.8em; +} + +:host-context(.linux) .monaco-menu .monaco-action-bar.vertical .submenu-indicator { + height: 100%; + mask-size: 10px 10px; + -webkit-mask-size: 10px 10px; +} + +.monaco-menu .action-item { + cursor: default; +} + +/* Arrows */ +.monaco-scrollable-element > .scrollbar > .scra { + cursor: pointer; + font-size: 11px !important; +} + +.monaco-scrollable-element > .visible { + opacity: 1; + + /* Background rule added for IE9 - to allow clicks on dom node */ + background:rgba(0,0,0,0); + + transition: opacity 100ms linear; +} +.monaco-scrollable-element > .invisible { + opacity: 0; + pointer-events: none; +} +.monaco-scrollable-element > .invisible.fade { + transition: opacity 800ms linear; +} + +/* Scrollable Content Inset Shadow */ +.monaco-scrollable-element > .shadow { + position: absolute; + display: none; +} +.monaco-scrollable-element > .shadow.top { + display: block; + top: 0; + left: 3px; + height: 3px; + width: 100%; + box-shadow: #DDD 0 6px 6px -6px inset; +} +.monaco-scrollable-element > .shadow.left { + display: block; + top: 3px; + left: 0; + height: 100%; + width: 3px; + box-shadow: #DDD 6px 0 6px -6px inset; +} +.monaco-scrollable-element > .shadow.top-left-corner { + display: block; + top: 0; + left: 0; + height: 3px; + width: 3px; +} +.monaco-scrollable-element > .shadow.top.left { + box-shadow: #DDD 6px 6px 6px -6px inset; +} + +/* ---------- Default Style ---------- */ + +:host-context(.vs) .monaco-scrollable-element > .scrollbar > .slider { + background: rgba(100, 100, 100, .4); +} +:host-context(.vs-dark) .monaco-scrollable-element > .scrollbar > .slider { + background: rgba(121, 121, 121, .4); +} +:host-context(.hc-black) .monaco-scrollable-element > .scrollbar > .slider { + background: rgba(111, 195, 223, .6); +} + +.monaco-scrollable-element > .scrollbar > .slider:hover { + background: rgba(100, 100, 100, .7); +} +:host-context(.hc-black) .monaco-scrollable-element > .scrollbar > .slider:hover { + background: rgba(111, 195, 223, .8); +} + +.monaco-scrollable-element > .scrollbar > .slider.active { + background: rgba(0, 0, 0, .6); +} +:host-context(.vs-dark) .monaco-scrollable-element > .scrollbar > .slider.active { + background: rgba(191, 191, 191, .4); +} +:host-context(.hc-black) .monaco-scrollable-element > .scrollbar > .slider.active { + background: rgba(111, 195, 223, 1); +} + +:host-context(.vs-dark) .monaco-scrollable-element .shadow.top { + box-shadow: none; +} + +:host-context(.vs-dark) .monaco-scrollable-element .shadow.left { + box-shadow: #000 6px 0 6px -6px inset; +} + +:host-context(.vs-dark) .monaco-scrollable-element .shadow.top.left { + box-shadow: #000 6px 6px 6px -6px inset; +} + +:host-context(.hc-black) .monaco-scrollable-element .shadow.top { + box-shadow: none; +} + +:host-context(.hc-black) .monaco-scrollable-element .shadow.left { + box-shadow: none; +} + +:host-context(.hc-black) .monaco-scrollable-element .shadow.top.left { + box-shadow: none; +} +`; diff --git a/src/vs/base/browser/ui/menu/menubar.css b/src/vs/base/browser/ui/menu/menubar.css new file mode 100644 index 00000000000..d815cfeddb9 --- /dev/null +++ b/src/vs/base/browser/ui/menu/menubar.css @@ -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. + *--------------------------------------------------------------------------------------------*/ + +/* Menubar styles */ + +.menubar { + display: flex; + flex-shrink: 1; + box-sizing: border-box; + height: 30px; + overflow: hidden; + flex-wrap: wrap; +} + +.fullscreen .menubar:not(.compact) { + margin: 0px; + padding: 0px 5px; +} + +.menubar > .menubar-menu-button { + align-items: center; + box-sizing: border-box; + padding: 0px 8px; + cursor: default; + -webkit-app-region: no-drag; + zoom: 1; + white-space: nowrap; + outline: 0; +} + +.menubar.compact { + flex-shrink: 0; + overflow: visible; /* to avoid the compact menu to be repositioned when clicking */ +} + +.menubar.compact > .menubar-menu-button { + width: 100%; + height: 100%; + padding: 0px; +} + +.menubar .menubar-menu-items-holder { + position: absolute; + left: 0px; + opacity: 1; + z-index: 2000; +} + +.menubar .menubar-menu-items-holder.monaco-menu-container { + outline: 0; + border: none; +} + +.menubar .menubar-menu-items-holder.monaco-menu-container :focus { + outline: 0; +} + +.menubar .toolbar-toggle-more { + width: 20px; + height: 100%; +} + +.menubar.compact .toolbar-toggle-more { + position: relative; + left: 0px; + top: 0px; + cursor: pointer; + width: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.menubar .toolbar-toggle-more { + padding: 0; + vertical-align: sub; +} + +.menubar.compact .toolbar-toggle-more::before { + content: "\eb94" !important; +} diff --git a/src/vs/base/browser/ui/menu/menubar.ts b/src/vs/base/browser/ui/menu/menubar.ts index d4e75e6df39..65249946405 100644 --- a/src/vs/base/browser/ui/menu/menubar.ts +++ b/src/vs/base/browser/ui/menu/menubar.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./menubar'; import * as browser from 'vs/base/browser/browser'; import * as DOM from 'vs/base/browser/dom'; import * as strings from 'vs/base/common/strings'; @@ -10,8 +11,8 @@ import * as nls from 'vs/nls'; import { domEvent } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventType, Gesture, GestureEvent } from 'vs/base/browser/touch'; -import { cleanMnemonic, IMenuOptions, Menu, MENU_ESCAPED_MNEMONIC_REGEX, MENU_MNEMONIC_REGEX, SubmenuAction, IMenuStyles, Direction } from 'vs/base/browser/ui/menu/menu'; -import { ActionRunner, IAction, IActionRunner } from 'vs/base/common/actions'; +import { cleanMnemonic, IMenuOptions, Menu, MENU_ESCAPED_MNEMONIC_REGEX, MENU_MNEMONIC_REGEX, IMenuStyles, Direction } from 'vs/base/browser/ui/menu/menu'; +import { ActionRunner, IAction, IActionRunner, SubmenuAction, Separator } from 'vs/base/common/actions'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import { KeyCode, ResolvedKeybinding, KeyMod } from 'vs/base/common/keyCodes'; @@ -21,7 +22,6 @@ import { asArray } from 'vs/base/common/arrays'; import { ScanCodeUtils, ScanCode } from 'vs/base/common/scanCode'; import { isMacintosh } from 'vs/base/common/platform'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { Codicon, registerIcon } from 'vs/base/common/codicons'; const $ = DOM.$; @@ -39,7 +39,7 @@ export interface IMenuBarOptions { } export interface MenuBarMenu { - actions: ReadonlyArray; + actions: IAction[]; label: string; } @@ -58,7 +58,7 @@ export class MenuBar extends Disposable { buttonElement: HTMLElement; titleElement: HTMLElement; label: string; - actions?: ReadonlyArray; + actions?: IAction[]; }[]; private overflowMenu!: { @@ -505,7 +505,7 @@ export class MenuBar extends Disposable { this.overflowMenu.actions = []; for (let idx = this.numMenusShown; idx < this.menuCache.length; idx++) { - this.overflowMenu.actions.push(new SubmenuAction(this.menuCache[idx].label, this.menuCache[idx].actions || [])); + this.overflowMenu.actions.push(new SubmenuAction(`menubar.submenu.${this.menuCache[idx].label}`, this.menuCache[idx].label, this.menuCache[idx].actions || [])); } if (this.overflowMenu.buttonElement.nextElementSibling !== this.menuCache[this.numMenusShown].buttonElement) { diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index a8f9bb24c5a..c54ff189085 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -215,7 +215,8 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi // Intercept keyboard handling - this._register(dom.addDisposableListener(this.selectElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { + // 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) => { const event = new StandardKeyboardEvent(e); let showDropDown = false; @@ -232,7 +233,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi if (showDropDown) { this.showSelectDropDown(); - dom.EventHelper.stop(e); + dom.EventHelper.stop(e, true); } })); } diff --git a/src/vs/base/browser/ui/toolbar/toolbar.ts b/src/vs/base/browser/ui/toolbar/toolbar.ts index a8b39211780..4210cded15c 100644 --- a/src/vs/base/browser/ui/toolbar/toolbar.ts +++ b/src/vs/base/browser/ui/toolbar/toolbar.ts @@ -5,17 +5,16 @@ import 'vs/css!./toolbar'; import * as nls from 'vs/nls'; -import { Action, IActionRunner, IAction } from 'vs/base/common/actions'; -import { ActionBar, ActionsOrientation, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IContextMenuProvider, DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdown'; +import { Action, IActionRunner, IAction, IActionViewItemProvider, SubmenuAction } from 'vs/base/common/actions'; +import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; -import { Disposable, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { withNullAsUndefined } from 'vs/base/common/types'; import { Codicon, registerIcon } from 'vs/base/common/codicons'; -import { Emitter } from 'vs/base/common/event'; - -export const CONTEXT = 'context.toolbar'; +import { EventMultiplexer } from 'vs/base/common/event'; +import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; +import { IContextMenuProvider } from 'vs/base/browser/contextmenu'; const toolBarMoreIcon = registerIcon('toolbar-more', Codicon.more); @@ -37,12 +36,14 @@ export class ToolBar extends Disposable { private actionBar: ActionBar; private toggleMenuAction: ToggleMenuAction; private toggleMenuActionViewItem: DropdownMenuActionViewItem | undefined; - private toggleMenuActionViewItemDisposable: IDisposable = Disposable.None; + private submenuActionViewItems: DropdownMenuActionViewItem[] = []; private hasSecondaryActions: boolean = false; private lookupKeybindings: boolean; + private element: HTMLElement; - private _onDidChangeDropdownVisibility = this._register(new Emitter()); + private _onDidChangeDropdownVisibility = this._register(new EventMultiplexer()); readonly onDidChangeDropdownVisibility = this._onDidChangeDropdownVisibility.event; + private disposables = new DisposableStore(); constructor(container: HTMLElement, contextMenuProvider: IContextMenuProvider, options: IToolBarOptions = { orientation: ActionsOrientation.HORIZONTAL }) { super(); @@ -52,44 +53,66 @@ export class ToolBar extends Disposable { this.toggleMenuAction = this._register(new ToggleMenuAction(() => this.toggleMenuActionViewItem?.show(), options.toggleMenuTitle)); - let element = document.createElement('div'); - element.className = 'monaco-toolbar'; - container.appendChild(element); + this.element = document.createElement('div'); + this.element.className = 'monaco-toolbar'; + container.appendChild(this.element); - this.actionBar = this._register(new ActionBar(element, { + this.actionBar = this._register(new ActionBar(this.element, { orientation: options.orientation, ariaLabel: options.ariaLabel, actionRunner: options.actionRunner, actionViewItemProvider: (action: IAction) => { - - // Return special action item for the toggle menu action if (action.id === ToggleMenuAction.ID) { - - this.toggleMenuActionViewItemDisposable.dispose(); - - // Create new this.toggleMenuActionViewItem = new DropdownMenuActionViewItem( action, (action).menuActions, contextMenuProvider, - this.options.actionViewItemProvider, - this.actionRunner, - this.options.getKeyBinding, - toolBarMoreIcon.classNames, - this.options.anchorAlignmentProvider, - true + { + actionViewItemProvider: this.options.actionViewItemProvider, + actionRunner: this.actionRunner, + keybindingProvider: this.options.getKeyBinding, + classNames: toolBarMoreIcon.classNames, + anchorAlignmentProvider: this.options.anchorAlignmentProvider, + menuAsChild: true + } ); this.toggleMenuActionViewItem.setActionContext(this.actionBar.context); - - this.toggleMenuActionViewItemDisposable = combinedDisposable( - this.toggleMenuActionViewItem, - this.toggleMenuActionViewItem.onDidChangeVisibility(e => this._onDidChangeDropdownVisibility.fire(e)) - ); + this.disposables.add(this._onDidChangeDropdownVisibility.add(this.toggleMenuActionViewItem.onDidChangeVisibility)); return this.toggleMenuActionViewItem; } - return options.actionViewItemProvider ? options.actionViewItemProvider(action) : undefined; + if (options.actionViewItemProvider) { + const result = options.actionViewItemProvider(action); + + if (result) { + return result; + } + } + + if (action instanceof SubmenuAction) { + const actions = Array.isArray(action.actions) ? action.actions : action.actions(); + const result = new DropdownMenuActionViewItem( + action, + actions, + contextMenuProvider, + { + actionViewItemProvider: this.options.actionViewItemProvider, + actionRunner: this.actionRunner, + keybindingProvider: this.options.getKeyBinding, + classNames: action.class, + anchorAlignmentProvider: this.options.anchorAlignmentProvider, + menuAsChild: true + } + ); + result.setActionContext(this.actionBar.context); + this.submenuActionViewItems.push(result); + this.disposables.add(this._onDidChangeDropdownVisibility.add(result.onDidChangeVisibility)); + + return result; + } + + return undefined; } })); } @@ -107,10 +130,13 @@ export class ToolBar extends Disposable { if (this.toggleMenuActionViewItem) { this.toggleMenuActionViewItem.setActionContext(context); } + for (const actionViewItem of this.submenuActionViewItems) { + actionViewItem.setActionContext(context); + } } - getContainer(): HTMLElement { - return this.actionBar.getContainer(); + getElement(): HTMLElement { + return this.element; } getItemsWidth(): number { @@ -126,6 +152,8 @@ export class ToolBar extends Disposable { } setActions(primaryActions: ReadonlyArray, secondaryActions?: ReadonlyArray): void { + this.clear(); + let primaryActionsToSet = primaryActions ? primaryActions.slice(0) : []; // Inject additional action to open secondary actions if present @@ -135,8 +163,6 @@ export class ToolBar extends Disposable { primaryActionsToSet.push(this.toggleMenuAction); } - this.actionBar.clear(); - primaryActionsToSet.forEach(action => { this.actionBar.push(action, { icon: true, label: false, keybinding: this.getKeybindingLabel(action) }); }); @@ -148,25 +174,15 @@ export class ToolBar extends Disposable { return withNullAsUndefined(key?.getLabel()); } - addPrimaryAction(primaryAction: IAction): () => void { - return () => { - - // Add after the "..." action if we have secondary actions - if (this.hasSecondaryActions) { - let itemCount = this.actionBar.length(); - this.actionBar.push(primaryAction, { icon: true, label: false, index: itemCount, keybinding: this.getKeybindingLabel(primaryAction) }); - } - - // Otherwise just add to the end - else { - this.actionBar.push(primaryAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(primaryAction) }); - } - }; + private clear(): void { + this.submenuActionViewItems = []; + this.disposables.clear(); + this.actionBar.clear(); } dispose(): void { + this.clear(); super.dispose(); - this.toggleMenuActionViewItemDisposable.dispose(); } } diff --git a/src/vs/base/common/actions.ts b/src/vs/base/common/actions.ts index 135008aa81d..a3b396524eb 100644 --- a/src/vs/base/common/actions.ts +++ b/src/vs/base/common/actions.ts @@ -39,14 +39,18 @@ export interface IActionRunner extends IDisposable { } export interface IActionViewItem extends IDisposable { - readonly actionRunner: IActionRunner; + actionRunner: IActionRunner; setActionContext(context: any): void; render(element: any /* HTMLElement */): void; isEnabled(): boolean; - focus(): void; + focus(fromRight?: boolean): void; // TODO@isidorn what is this? blur(): void; } +export interface IActionViewItemProvider { + (action: IAction): IActionViewItem | undefined; +} + export interface IActionChangeEvent { readonly label?: string; readonly tooltip?: string; @@ -218,3 +222,22 @@ export class RadioGroup extends Disposable { } } } + +export class Separator extends Action { + + static readonly ID = 'vs.actions.separator'; + + constructor(label?: string) { + super(Separator.ID, label, label ? 'separator text' : 'separator'); + this.checked = false; + this.enabled = false; + } +} + +export type SubmenuActions = IAction[] | (() => IAction[]); + +export class SubmenuAction extends Action { + constructor(id: string, label: string, readonly 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 77fc4a4fe36..cf9786a2076 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -473,11 +473,10 @@ export function range(arg: number, to?: number): number[] { } export function index(array: ReadonlyArray, indexer: (t: T) => string): { [key: string]: T; }; -export function index(array: ReadonlyArray, indexer: (t: T) => string, merger?: (t: T, r: R) => R): { [key: string]: R; }; -export function index(array: ReadonlyArray, indexer: (t: T) => string, merger: (t: T, r: R) => R = t => t as any): { [key: string]: R; } { +export function index(array: ReadonlyArray, indexer: (t: T) => string, mapper: (t: T) => R): { [key: string]: R; }; +export function index(array: ReadonlyArray, indexer: (t: T) => string, mapper?: (t: T) => R): { [key: string]: R; } { return array.reduce((r, t) => { - const key = indexer(t); - r[key] = merger(t, r[key]); + r[indexer(t)] = mapper ? mapper(t) : t; return r; }, Object.create(null)); } diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index 3f3177206e3..36696e4a455 100644 --- a/src/vs/base/common/codicons.ts +++ b/src/vs/base/common/codicons.ts @@ -476,6 +476,7 @@ export namespace Codicon { export const debugAltSmall = new Codicon('debug-alt-small', { character: '\\eba8' }); export const vmConnect = new Codicon('vm-connect', { character: '\\eba9' }); export const cloud = new Codicon('cloud', { character: '\\ebaa' }); + export const merge = new Codicon('merge', { character: '\\ebab' }); } diff --git a/src/vs/base/common/json.ts b/src/vs/base/common/json.ts index df362c06589..933e370f92c 100644 --- a/src/vs/base/common/json.ts +++ b/src/vs/base/common/json.ts @@ -72,6 +72,7 @@ export interface JSONScanner { } + export interface ParseError { error: ParseErrorCode; offset: number; diff --git a/src/vs/base/common/marshalling.ts b/src/vs/base/common/marshalling.ts index 335ee5691e0..e76ba91738f 100644 --- a/src/vs/base/common/marshalling.ts +++ b/src/vs/base/common/marshalling.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; +import { VSBuffer } from 'vs/base/common/buffer'; import { regExpFlags } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; export function stringify(obj: any): string { return JSON.stringify(obj, replacer); @@ -44,10 +45,23 @@ export function revive(obj: any, depth = 0): any { case 2: return new RegExp(obj.source, obj.flags); } - // walk object (or array) - for (let key in obj) { - if (Object.hasOwnProperty.call(obj, key)) { - obj[key] = revive(obj[key], depth + 1); + if ( + obj instanceof VSBuffer + || obj instanceof Uint8Array + ) { + return obj; + } + + if (Array.isArray(obj)) { + for (let i = 0; i < obj.length; ++i) { + obj[i] = revive(obj[i], depth + 1); + } + } else { + // walk object + for (const key in obj) { + if (Object.hasOwnProperty.call(obj, key)) { + obj[key] = revive(obj[key], depth + 1); + } } } } diff --git a/src/vs/base/common/scrollable.ts b/src/vs/base/common/scrollable.ts index a45e0763ab1..cb3fe20dc79 100644 --- a/src/vs/base/common/scrollable.ts +++ b/src/vs/base/common/scrollable.ts @@ -332,6 +332,12 @@ export class Scrollable extends Disposable { this._setState(newState); + if (!this._smoothScrolling) { + // Looks like someone canceled the smooth scrolling + // from the scroll event handler + return; + } + if (update.isDone) { this._smoothScrolling.dispose(); this._smoothScrolling = null; diff --git a/src/vs/base/node/terminalEncoding.ts b/src/vs/base/node/terminalEncoding.ts index 63652abb1e5..fdfc268c48a 100644 --- a/src/vs/base/node/terminalEncoding.ts +++ b/src/vs/base/node/terminalEncoding.ts @@ -61,6 +61,10 @@ export async function resolveTerminalEncoding(verbose?: boolean): Promise { if (stdout) { + if (verbose) { + console.log(`Output from "chcp" command is: ${stdout}`); + } + const windowsTerminalEncodingKeys = Object.keys(windowsTerminalEncodings) as Array; for (const key of windowsTerminalEncodingKeys) { if (stdout.indexOf(key) >= 0) { diff --git a/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts b/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts index 7b43355ed6d..6222be78028 100644 --- a/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts +++ b/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts @@ -9,10 +9,9 @@ import { ISerializableContextMenuItem, CONTEXT_MENU_CLOSE_CHANNEL, CONTEXT_MENU_ 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); - const window = BrowserWindow.fromWebContents(event.sender); menu.popup({ - window: window ? window : undefined, + window: 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/ipc/common/ipc.net.ts b/src/vs/base/parts/ipc/common/ipc.net.ts index 132654b320f..c3fede90bf3 100644 --- a/src/vs/base/parts/ipc/common/ipc.net.ts +++ b/src/vs/base/parts/ipc/common/ipc.net.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { IMessagePassingProtocol, IPCClient } from 'vs/base/parts/ipc/common/ipc'; +import { IMessagePassingProtocol, IPCClient, IIPCLogger } from 'vs/base/parts/ipc/common/ipc'; import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; import * as platform from 'vs/base/common/platform'; @@ -16,6 +16,7 @@ export interface ISocket extends IDisposable { onEnd(listener: () => void): IDisposable; write(buffer: VSBuffer): void; end(): void; + drain(): Promise; } let emptyBuffer: VSBuffer | null = null; @@ -277,6 +278,11 @@ class ProtocolWriter { this._isDisposed = true; } + public drain(): Promise { + this.flush(); + return this._socket.drain(); + } + public flush(): void { // flush this._writeNow(); @@ -372,6 +378,10 @@ export class Protocol extends Disposable implements IMessagePassingProtocol { this._register(this._socket.onClose(() => this._onClose.fire())); } + drain(): Promise { + return this._socketWriter.drain(); + } + getSocket(): ISocket { return this._socket; } @@ -393,8 +403,8 @@ export class Client extends IPCClient { get onClose(): Event { return this.protocol.onClose; } - constructor(private protocol: Protocol | PersistentProtocol, id: TContext) { - super(protocol, id); + constructor(private protocol: Protocol | PersistentProtocol, id: TContext, ipcLogger: IIPCLogger | null = null) { + super(protocol, id, ipcLogger); } dispose(): void { @@ -619,6 +629,10 @@ export class PersistentProtocol implements IMessagePassingProtocol { this._socketDisposables = dispose(this._socketDisposables); } + drain(): Promise { + return this._socketWriter.drain(); + } + sendDisconnect(): void { const msg = new ProtocolMessage(ProtocolMessageType.Disconnect, 0, 0, getEmptyBuffer()); this._socketWriter.write(msg); diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index 411bfe46cca..ad2e7066eff 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -12,7 +12,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { getRandomElement } from 'vs/base/common/arrays'; import { isFunction, isUndefinedOrNull } from 'vs/base/common/types'; import { revive } from 'vs/base/common/marshalling'; -import { isUpperAsciiLetter } from 'vs/base/common/strings'; +import * as strings from 'vs/base/common/strings'; /** * An `IChannel` is an abstraction over a collection of commands. @@ -42,6 +42,19 @@ export const enum RequestType { EventDispose = 103 } +function requestTypeToStr(type: RequestType): string { + switch (type) { + case RequestType.Promise: + return 'req'; + case RequestType.PromiseCancel: + return 'cancel'; + case RequestType.EventListen: + return 'subscribe'; + case RequestType.EventDispose: + return 'unsubscribe'; + } +} + type IRawPromiseRequest = { type: RequestType.Promise; id: number; channelName: string; name: string; arg: any; }; type IRawPromiseCancelRequest = { type: RequestType.PromiseCancel, id: number }; type IRawEventListenRequest = { type: RequestType.EventListen; id: number; channelName: string; name: string; arg: any; }; @@ -56,6 +69,20 @@ export const enum ResponseType { EventFire = 204 } +function responseTypeToStr(type: ResponseType): string { + switch (type) { + case ResponseType.Initialize: + return `init`; + case ResponseType.PromiseSuccess: + return `reply:`; + case ResponseType.PromiseError: + case ResponseType.PromiseErrorObj: + return `replyErr:`; + case ResponseType.EventFire: + return `event:`; + } +} + type IRawInitializeResponse = { type: ResponseType.Initialize }; type IRawPromiseSuccessResponse = { type: ResponseType.PromiseSuccess; id: number; data: any }; type IRawPromiseErrorResponse = { type: ResponseType.PromiseError; id: number; data: { message: string, name: string, stack: string[] | undefined } }; @@ -70,6 +97,10 @@ interface IHandler { export interface IMessagePassingProtocol { send(buffer: VSBuffer): void; onMessage: Event; + /** + * Wait for the write buffer (if applicable) to become empty. + */ + drain?(): Promise; } enum State { @@ -264,7 +295,7 @@ export class ChannelServer implements IChannelServer(); - constructor(private protocol: IMessagePassingProtocol, private ctx: TContext, private timeoutDelay: number = 1000) { + constructor(private protocol: IMessagePassingProtocol, private ctx: TContext, private logger: IIPCLogger | null = null, private timeoutDelay: number = 1000) { this.protocolListener = this.protocol.onMessage(msg => this.onRawMessage(msg)); this.sendResponse({ type: ResponseType.Initialize }); } @@ -278,29 +309,41 @@ export class ChannelServer implements IChannelServer implements IChannelServer implements IChannelServer(); private lastRequestId: number = 0; private protocolListener: IDisposable | null; + private logger: IIPCLogger | null; private readonly _onDidInitialize = new Emitter(); readonly onDidInitialize = this._onDidInitialize.event; - constructor(private protocol: IMessagePassingProtocol) { + constructor(private protocol: IMessagePassingProtocol, logger: IIPCLogger | null = null) { this.protocolListener = this.protocol.onMessage(msg => this.onBuffer(msg)); + this.logger = logger; } getChannel(channelName: string): T { @@ -482,10 +549,7 @@ export class ChannelClient implements IChannelClient, IDisposable { return e(errors.canceled()); } - let uninitializedPromise: CancelablePromise | null = createCancelablePromise(_ => this.whenInitialized()); - uninitializedPromise.then(() => { - uninitializedPromise = null; - + const doRequest = () => { const handler: IHandler = response => { switch (response.type) { case ResponseType.PromiseSuccess: @@ -510,7 +574,18 @@ export class ChannelClient implements IChannelClient, IDisposable { this.handlers.set(id, handler); this.sendRequest(request); - }); + }; + + let uninitializedPromise: CancelablePromise | null = null; + if (this.state === State.Idle) { + doRequest(); + } else { + uninitializedPromise = createCancelablePromise(_ => this.whenInitialized()); + uninitializedPromise.then(() => { + uninitializedPromise = null; + doRequest(); + }); + } const cancel = () => { if (uninitializedPromise) { @@ -567,27 +642,39 @@ export class ChannelClient implements IChannelClient, IDisposable { private sendRequest(request: IRawRequest): void { switch (request.type) { case RequestType.Promise: - case RequestType.EventListen: - return this.send([request.type, request.id, request.channelName, request.name], request.arg); + case RequestType.EventListen: { + const msgLength = this.send([request.type, request.id, request.channelName, request.name], request.arg); + if (this.logger) { + this.logger.logOutgoing(msgLength, request.id, RequestInitiator.LocalSide, `${requestTypeToStr(request.type)}: ${request.channelName}.${request.name}`, request.arg); + } + return; + } case RequestType.PromiseCancel: - case RequestType.EventDispose: - return this.send([request.type, request.id]); + case RequestType.EventDispose: { + const msgLength = this.send([request.type, request.id]); + if (this.logger) { + this.logger.logOutgoing(msgLength, request.id, RequestInitiator.LocalSide, requestTypeToStr(request.type)); + } + return; + } } } - private send(header: any, body: any = undefined): void { + private send(header: any, body: any = undefined): number { const writer = new BufferWriter(); serialize(writer, header); serialize(writer, body); - this.sendBuffer(writer.buffer); + return this.sendBuffer(writer.buffer); } - private sendBuffer(message: VSBuffer): void { + private sendBuffer(message: VSBuffer): number { try { this.protocol.send(message); + return message.byteLength; } catch (err) { // noop + return 0; } } @@ -599,12 +686,18 @@ export class ChannelClient implements IChannelClient, IDisposable { switch (type) { case ResponseType.Initialize: + if (this.logger) { + this.logger.logIncoming(message.byteLength, 0, RequestInitiator.LocalSide, responseTypeToStr(type)); + } return this.onResponse({ type: header[0] }); case ResponseType.PromiseSuccess: case ResponseType.PromiseError: case ResponseType.EventFire: case ResponseType.PromiseErrorObj: + if (this.logger) { + this.logger.logIncoming(message.byteLength, header[1], RequestInitiator.LocalSide, responseTypeToStr(type), body); + } return this.onResponse({ type: header[0], id: header[1], data: body }); } } @@ -832,13 +925,13 @@ export class IPCClient implements IChannelClient, IChannelSer private channelClient: ChannelClient; private channelServer: ChannelServer; - constructor(protocol: IMessagePassingProtocol, ctx: TContext) { + constructor(protocol: IMessagePassingProtocol, ctx: TContext, ipcLogger: IIPCLogger | null = null) { const writer = new BufferWriter(); serialize(writer, ctx); protocol.send(writer.buffer); - this.channelClient = new ChannelClient(protocol); - this.channelServer = new ChannelServer(protocol, ctx); + this.channelClient = new ChannelClient(protocol, ipcLogger); + this.channelServer = new ChannelServer(protocol, ctx, ipcLogger); } getChannel(channelName: string): T { @@ -1056,7 +1149,68 @@ export function createChannelSender(channel: IChannel, options?: IChannelSend function propertyIsEvent(name: string): boolean { // Assume a property is an event if it has a form of "onSomething" - return name[0] === 'o' && name[1] === 'n' && isUpperAsciiLetter(name.charCodeAt(2)); + return name[0] === 'o' && name[1] === 'n' && strings.isUpperAsciiLetter(name.charCodeAt(2)); } //#endregion + + +const colorTables = [ + ['#2977B1', '#FC802D', '#34A13A', '#D3282F', '#9366BA'], + ['#8B564C', '#E177C0', '#7F7F7F', '#BBBE3D', '#2EBECD'] +]; + +function prettyWithoutArrays(data: any): any { + if (Array.isArray(data)) { + return data; + } + if (data && typeof data === 'object' && typeof data.toString === 'function') { + let result = data.toString(); + if (result !== '[object Object]') { + return result; + } + } + return data; +} + +function pretty(data: any): any { + if (Array.isArray(data)) { + return data.map(prettyWithoutArrays); + } + return prettyWithoutArrays(data); +} + +export function logWithColors(direction: string, totalLength: number, msgLength: number, req: number, initiator: RequestInitiator, str: string, data: any): void { + data = pretty(data); + + const colorTable = colorTables[initiator]; + const color = colorTable[req % colorTable.length]; + let args = [`%c[${direction}]%c[${strings.pad(totalLength, 7, ' ')}]%c[len: ${strings.pad(msgLength, 5, ' ')}]%c${strings.pad(req, 5, ' ')} - ${str}`, 'color: darkgreen', 'color: grey', 'color: grey', `color: ${color}`]; + if (/\($/.test(str)) { + args = args.concat(data); + args.push(')'); + } else { + args.push(data); + } + console.log.apply(console, args as [string, ...string[]]); +} + +export class IPCLogger implements IIPCLogger { + private _totalIncoming = 0; + private _totalOutgoing = 0; + + constructor( + private readonly _outgoingPrefix: string, + private readonly _incomingPrefix: string, + ) { } + + public logOutgoing(msgLength: number, requestId: number, initiator: RequestInitiator, str: string, data?: any): void { + this._totalOutgoing += msgLength; + logWithColors(this._outgoingPrefix, this._totalOutgoing, msgLength, requestId, initiator, str, data); + } + + public logIncoming(msgLength: number, requestId: number, initiator: RequestInitiator, str: string, data?: any): void { + this._totalIncoming += msgLength; + logWithColors(this._incomingPrefix, this._totalIncoming, msgLength, requestId, initiator, str, data); + } +} diff --git a/src/vs/base/parts/ipc/node/ipc.net.ts b/src/vs/base/parts/ipc/node/ipc.net.ts index ec80ba3f1c3..2b6c70afa70 100644 --- a/src/vs/base/parts/ipc/node/ipc.net.ts +++ b/src/vs/base/parts/ipc/node/ipc.net.ts @@ -12,6 +12,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; import { ISocket, Protocol, Client, ChunkStream } from 'vs/base/parts/ipc/common/ipc.net'; +import { onUnexpectedError } from 'vs/base/common/errors'; export class NodeSocket implements ISocket { public readonly socket: Socket; @@ -57,12 +58,47 @@ export class NodeSocket implements ISocket { // > https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback // > However, the false return value is only advisory and the writable stream will unconditionally // > accept and buffer chunk even if it has not been allowed to drain. - this.socket.write(buffer.buffer); + try { + this.socket.write(buffer.buffer); + } catch (err) { + if (err.code === 'EPIPE') { + // An EPIPE exception at the wrong time can lead to a renderer process crash + // so ignore the error since the socket will fire the close event soon anyways: + // > https://nodejs.org/api/errors.html#errors_common_system_errors + // > EPIPE (Broken pipe): A write on a pipe, socket, or FIFO for which there is no + // > process to read the data. Commonly encountered at the net and http layers, + // > indicative that the remote side of the stream being written to has been closed. + return; + } + onUnexpectedError(err); + } } public end(): void { this.socket.end(); } + + public drain(): Promise { + return new Promise((resolve, reject) => { + if (this.socket.bufferSize === 0) { + resolve(); + return; + } + const finished = () => { + this.socket.off('close', finished); + this.socket.off('end', finished); + this.socket.off('error', finished); + this.socket.off('timeout', finished); + this.socket.off('drain', finished); + resolve(); + }; + this.socket.on('close', finished); + this.socket.on('end', finished); + this.socket.on('error', finished); + this.socket.on('timeout', finished); + this.socket.on('drain', finished); + }); + } } const enum Constants { @@ -229,6 +265,10 @@ export class WebSocketNodeSocket extends Disposable implements ISocket { } } } + + public drain(): Promise { + return this.socket.drain(); + } } function unmask(buffer: VSBuffer, mask: number): void { diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index afa9d333003..25475e8e20d 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -18,7 +18,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Button, IButtonStyles } from 'vs/base/browser/ui/button/button'; import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import Severity from 'vs/base/common/severity'; -import { ActionBar, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action } from 'vs/base/common/actions'; import { equals } from 'vs/base/common/arrays'; import { TimeoutTimer } from 'vs/base/common/async'; @@ -28,6 +28,7 @@ import { List, IListOptions, IListStyles } from 'vs/base/browser/ui/list/listWid import { IInputBoxStyles } from 'vs/base/browser/ui/inputbox/inputBox'; import { Color } from 'vs/base/common/color'; import { registerIcon, Codicon } from 'vs/base/common/codicons'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; export interface IQuickInputOptions { idPrefix: string; diff --git a/src/vs/base/parts/sandbox/electron-browser/preload.js b/src/vs/base/parts/sandbox/electron-browser/preload.js index f0718ffc8e9..d10c4be3ae1 100644 --- a/src/vs/base/parts/sandbox/electron-browser/preload.js +++ b/src/vs/base/parts/sandbox/electron-browser/preload.js @@ -7,16 +7,13 @@ (function () { 'use strict'; - const { ipcRenderer, webFrame, crashReporter } = require('electron'); + const { ipcRenderer, webFrame, crashReporter, contextBridge } = require('electron'); - // @ts-ignore - window.vscode = { + const globals = { /** - * A minimal set of methods exposed from ipcRenderer - * to support communication to electron-main - * - * @type {typeof import('../electron-sandbox/globals').ipcRenderer} + * A minimal set of methods exposed from Electron's `ipcRenderer` + * to support communication to main process. */ ipcRenderer: { @@ -25,9 +22,9 @@ * @param {any[]} args */ send(channel, ...args) { - validateIPC(channel); - - ipcRenderer.send(channel, ...args); + if (validateIPC(channel)) { + ipcRenderer.send(channel, ...args); + } }, /** @@ -35,9 +32,9 @@ * @param {(event: import('electron').IpcRendererEvent, ...args: any[]) => void} listener */ on(channel, listener) { - validateIPC(channel); - - ipcRenderer.on(channel, listener); + if (validateIPC(channel)) { + ipcRenderer.on(channel, listener); + } }, /** @@ -45,9 +42,9 @@ * @param {(event: import('electron').IpcRendererEvent, ...args: any[]) => void} listener */ once(channel, listener) { - validateIPC(channel); - - ipcRenderer.once(channel, listener); + if (validateIPC(channel)) { + ipcRenderer.once(channel, listener); + } }, /** @@ -55,16 +52,14 @@ * @param {(event: import('electron').IpcRendererEvent, ...args: any[]) => void} listener */ removeListener(channel, listener) { - validateIPC(channel); - - ipcRenderer.removeListener(channel, listener); + if (validateIPC(channel)) { + ipcRenderer.removeListener(channel, listener); + } } }, /** - * Support for methods of webFrame type. - * - * @type {typeof import('../electron-sandbox/globals').webFrame} + * Support for subset of methods of Electron's `webFrame` type. */ webFrame: { @@ -72,14 +67,14 @@ * @param {number} level */ setZoomLevel(level) { - webFrame.setZoomLevel(level); + if (typeof level === 'number') { + webFrame.setZoomLevel(level); + } } }, /** - * Support for methods of crashReporter type. - * - * @type {typeof import('../electron-sandbox/globals').crashReporter} + * Support for subset of methods of Electron's `crashReporter` type. */ crashReporter: { @@ -89,9 +84,53 @@ start(options) { crashReporter.start(options); } + }, + + /** + * Support for a subset of access to node.js global `process`. + */ + process: { + platform: process.platform, + env: process.env, + on: + /** + * @param {string} type + * @param {() => void} callback + */ + function (type, callback) { + if (validateProcessEventType(type)) { + process.on(type, callback); + } + } + }, + + /** + * Some information about the context we are running in. + */ + context: { + sandbox: process.argv.includes('--enable-sandbox') } }; + // Use `contextBridge` APIs to expose globals to VSCode + // only if context isolation is enabled, otherwise just + // add to the DOM global. + let useContextBridge = process.argv.includes('--context-isolation'); + if (useContextBridge) { + try { + contextBridge.exposeInMainWorld('vscode', globals); + } catch (error) { + console.error(error); + + useContextBridge = false; + } + } + + if (!useContextBridge) { + // @ts-ignore + window.vscode = globals; + } + //#region Utilities /** @@ -101,6 +140,20 @@ if (!channel || !channel.startsWith('vscode:')) { throw new Error(`Unsupported event IPC channel '${channel}'`); } + + return true; + } + + /** + * @param {string} type + * @returns {type is 'uncaughtException'} + */ + function validateProcessEventType(type) { + if (type !== 'uncaughtException') { + throw new Error(`Unsupported process event '${type}'`); + } + + return true; } //#endregion diff --git a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts index 47d3700efd7..573e4735933 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts @@ -81,3 +81,30 @@ export const crashReporter = (window as any).vscode.crashReporter as { */ start(options: CrashReporterStartOptions): void; }; + +export const process = (window as any).vscode.process as { + + /** + * The process.platform property returns a string identifying the operating system platform + * on which the Node.js process is running. + */ + platform: 'win32' | 'linux' | 'darwin'; + + /** + * The process.env property returns an object containing the user environment. See environ(7). + */ + env: { [key: string]: string | undefined }; + + /** + * A listener on the process. Only a small subset of listener types are allowed. + */ + on: (type: string, callback: Function) => void; +}; + +export const context = (window as any).vscode.context as { + + /** + * Wether the renderer runs with `sandbox` enabled or not. + */ + sandbox: boolean; +}; diff --git a/src/vs/base/test/browser/actionbar.test.ts b/src/vs/base/test/browser/actionbar.test.ts index 0a57211472b..8915318f99a 100644 --- a/src/vs/base/test/browser/actionbar.test.ts +++ b/src/vs/base/test/browser/actionbar.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { Separator, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; -import { Action } from 'vs/base/common/actions'; +import { prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Action, Separator } from 'vs/base/common/actions'; suite('Actionbar', () => { diff --git a/src/vs/base/worker/defaultWorkerFactory.ts b/src/vs/base/worker/defaultWorkerFactory.ts index 45f3b41a457..1f2d5dbb79e 100644 --- a/src/vs/base/worker/defaultWorkerFactory.ts +++ b/src/vs/base/worker/defaultWorkerFactory.ts @@ -28,11 +28,11 @@ function getWorker(workerId: string, label: string): Worker | Promise { } // ESM-comment-begin -export function getWorkerBootstrapUrl(scriptPath: string, label: string): string { - if (/^(http:)|(https:)|(file:)/.test(scriptPath)) { +export function getWorkerBootstrapUrl(scriptPath: string, label: string, forceDataUri: boolean = false): string { + if (forceDataUri || /^((http:)|(https:)|(file:))/.test(scriptPath)) { const currentUrl = String(window.location); const currentOrigin = currentUrl.substr(0, currentUrl.length - window.location.hash.length - window.location.search.length - window.location.pathname.length); - if (scriptPath.substring(0, currentOrigin.length) !== currentOrigin) { + if (forceDataUri || scriptPath.substring(0, currentOrigin.length) !== currentOrigin) { // this is the cross-origin case // i.e. the webpage is running at a different origin than where the scripts are loaded from const myPath = 'vs/base/worker/defaultWorkerFactory.js'; diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index 556c03a03ab..c629f7fffa1 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -12,6 +12,7 @@ import { request } from 'vs/base/parts/request/browser/request'; import { isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/windows/common/windows'; import { isEqual } from 'vs/base/common/resources'; import { isStandalone } from 'vs/base/browser/browser'; +import { localize } from 'vs/nls'; interface ICredential { service: string; @@ -345,6 +346,11 @@ class WorkspaceProvider implements IWorkspaceProvider { // Finally create workbench create(document.body, { ...config, + homeIndicator: { + href: 'https://github.com/Microsoft/vscode', + icon: 'code', + title: localize('home', "Home") + }, workspaceProvider: new WorkspaceProvider(workspace, payload), urlCallbackProvider: new PollingURLCallbackProvider(), credentialsProvider: new LocalStorageCredentialsProvider() diff --git a/src/vs/code/buildfile.js b/src/vs/code/buildfile.js index 221bea21443..9b1ee16680e 100644 --- a/src/vs/code/buildfile.js +++ b/src/vs/code/buildfile.js @@ -22,10 +22,9 @@ exports.collectModules = function () { createModuleDescription('vs/code/electron-main/main', []), createModuleDescription('vs/code/node/cli', []), createModuleDescription('vs/code/node/cliProcessMain', ['vs/code/node/cli']), - createModuleDescription('vs/code/electron-browser/issue/issueReporterMain', []), + createModuleDescription('vs/code/electron-sandbox/issue/issueReporterMain', []), createModuleDescription('vs/code/electron-browser/sharedProcess/sharedProcessMain', []), - createModuleDescription('vs/code/electron-browser/issue/issueReporterMain', []), createModuleDescription('vs/platform/driver/node/driver', []), - createModuleDescription('vs/code/electron-browser/processExplorer/processExplorerMain', []) + createModuleDescription('vs/code/electron-sandbox/processExplorer/processExplorerMain', []) ]; }; diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 7aef5c8c7b7..1e8351687cf 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -49,10 +49,10 @@ import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; -import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; -import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel, StorageKeysSyncRegistryChannelClient, UserDataSyncMachinesServiceChannel, UserDataSyncAccountServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; +import { UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; +import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel, StorageKeysSyncRegistryChannelClient, UserDataSyncMachinesServiceChannel, UserDataSyncAccountServiceChannel, UserDataSyncStoreManagementServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; @@ -202,6 +202,7 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(IUserDataSyncLogService, new SyncDescriptor(UserDataSyncLogService)); services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(server.getChannel('userDataSyncUtil', client => client.ctx !== 'main'))); services.set(IGlobalExtensionEnablementService, new SyncDescriptor(GlobalExtensionEnablementService)); + services.set(IUserDataSyncStoreManagementService, new SyncDescriptor(UserDataSyncStoreManagementService)); services.set(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService)); services.set(IUserDataSyncMachinesService, new SyncDescriptor(UserDataSyncMachinesService)); services.set(IUserDataSyncBackupStoreService, new SyncDescriptor(UserDataSyncBackupStoreService)); @@ -237,6 +238,10 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat const authTokenChannel = new UserDataSyncAccountServiceChannel(authTokenService); server.registerChannel('userDataSyncAccount', authTokenChannel); + const userDataSyncStoreManagementService = accessor.get(IUserDataSyncStoreManagementService); + const userDataSyncStoreManagementChannel = new UserDataSyncStoreManagementServiceChannel(userDataSyncStoreManagementService); + server.registerChannel('userDataSyncStoreManagement', userDataSyncStoreManagementChannel); + const userDataSyncService = accessor.get(IUserDataSyncService); const userDataSyncChannel = new UserDataSyncChannel(server, userDataSyncService, logService); server.registerChannel('userDataSync', userDataSyncChannel); diff --git a/src/vs/code/electron-main/auth.ts b/src/vs/code/electron-main/auth.ts index e03d4b8a527..c1e2a84946a 100644 --- a/src/vs/code/electron-main/auth.ts +++ b/src/vs/code/electron-main/auth.ts @@ -60,11 +60,11 @@ export class ProxyAuthHandler extends Disposable { title: 'VS Code', webPreferences: { preload: URI.parse(require.toUrl('vs/base/parts/sandbox/electron-browser/preload.js')).fsPath, - enableWebSQL: false, sandbox: true, - devTools: false, + contextIsolation: true, + enableWebSQL: false, enableRemoteModule: false, - v8CacheOptions: 'bypassHeatCheck' + devTools: false } }; diff --git a/src/vs/code/electron-main/sharedProcess.ts b/src/vs/code/electron-main/sharedProcess.ts index 92027912edc..d225e6eb31d 100644 --- a/src/vs/code/electron-main/sharedProcess.ts +++ b/src/vs/code/electron-main/sharedProcess.ts @@ -43,12 +43,12 @@ export class SharedProcess implements ISharedProcess { backgroundColor: this.themeMainService.getBackgroundColor(), webPreferences: { preload: URI.parse(require.toUrl('vs/base/parts/sandbox/electron-browser/preload.js')).fsPath, - images: false, nodeIntegration: true, - webgl: false, enableWebSQL: false, enableRemoteModule: false, nativeWindowOpen: true, + images: false, + webgl: false, disableBlinkFeatures: 'Auxclick' // do NOT change, allows us to identify this window as shared-process in the process explorer } }); diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index daf6df757cd..c300a83ac0e 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -168,10 +168,10 @@ export class CodeWindow extends Disposable implements ICodeWindow { webPreferences: { preload: URI.parse(this.doGetPreloadUrl()).fsPath, nodeIntegration: true, - webviewTag: true, enableWebSQL: false, enableRemoteModule: false, nativeWindowOpen: true, + webviewTag: true, zoomFactor: zoomLevelToZoomFactor(windowConfig?.zoomLevel) } }; diff --git a/src/vs/code/electron-browser/issue/issueReporter.html b/src/vs/code/electron-sandbox/issue/issueReporter.html similarity index 100% rename from src/vs/code/electron-browser/issue/issueReporter.html rename to src/vs/code/electron-sandbox/issue/issueReporter.html diff --git a/src/vs/code/electron-browser/issue/issueReporter.js b/src/vs/code/electron-sandbox/issue/issueReporter.js similarity index 92% rename from src/vs/code/electron-browser/issue/issueReporter.js rename to src/vs/code/electron-sandbox/issue/issueReporter.js index fdb59bf5fb8..6991c2bba2a 100644 --- a/src/vs/code/electron-browser/issue/issueReporter.js +++ b/src/vs/code/electron-sandbox/issue/issueReporter.js @@ -14,6 +14,6 @@ const bootstrapWindow = (() => { return window.MonacoBootstrapWindow; })(); -bootstrapWindow.load(['vs/code/electron-browser/issue/issueReporterMain'], function (issueReporter, configuration) { +bootstrapWindow.load(['vs/code/electron-sandbox/issue/issueReporterMain'], function (issueReporter, configuration) { issueReporter.startup(configuration); }, { forceEnableDeveloperKeybindings: true, disallowReloadKeybinding: true }); diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts similarity index 87% rename from src/vs/code/electron-browser/issue/issueReporterMain.ts rename to src/vs/code/electron-sandbox/issue/issueReporterMain.ts index dce3e411d83..9e7634b2791 100644 --- a/src/vs/code/electron-browser/issue/issueReporterMain.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts @@ -5,9 +5,8 @@ import 'vs/css!./media/issueReporter'; import 'vs/base/browser/ui/codicons/codiconStyles'; // make sure codicon css is loaded -import * as os from 'os'; import { ElectronService, IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; -import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { ipcRenderer, process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { applyZoom, zoomIn, zoomOut } from 'vs/platform/windows/electron-sandbox/window'; import { $, windowOpenNoOpener, addClass } from 'vs/base/browser/dom'; import { Button } from 'vs/base/browser/ui/button/button'; @@ -17,29 +16,15 @@ import { debounce } from 'vs/base/common/decorators'; import { Disposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { escape } from 'vs/base/common/strings'; -import { getDelayedChannel, createChannelSender } from 'vs/base/parts/ipc/common/ipc'; -import { connect as connectNet } from 'vs/base/parts/ipc/node/ipc.net'; import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; -import { IssueReporterData as IssueReporterModelData, IssueReporterModel } from 'vs/code/electron-browser/issue/issueReporterModel'; -import BaseHtml from 'vs/code/electron-browser/issue/issueReporterPage'; +import { IssueReporterData as IssueReporterModelData, IssueReporterModel } from 'vs/code/electron-sandbox/issue/issueReporterModel'; +import BaseHtml from 'vs/code/electron-sandbox/issue/issueReporterPage'; import { localize } from 'vs/nls'; import { isRemoteDiagnosticError, SystemInfo } from 'vs/platform/diagnostics/common/diagnostics'; -import { EnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; -import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IMainProcessService, MainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; -import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { ISettingsSearchIssueReporterData, IssueReporterData, IssueReporterExtensionData, IssueReporterFeatures, IssueReporterStyles, IssueType } from 'vs/platform/issue/common/issue'; -import { getLogLevel, ILogService } from 'vs/platform/log/common/log'; -import { FollowerLogService, LoggerChannelClient } from 'vs/platform/log/common/logIpc'; -import { SpdLogService } from 'vs/platform/log/node/spdlogService'; -import product from 'vs/platform/product/common/product'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ITelemetryServiceConfig, TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; -import { combinedAppender, LogAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; -import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc'; -import { INativeWindowConfiguration } from 'vs/platform/windows/node/window'; +import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; const MAX_URL_LENGTH = 2045; @@ -49,9 +34,23 @@ interface SearchResult { state?: string; } -export interface IssueReporterConfiguration extends INativeWindowConfiguration { +export interface IssueReporterConfiguration extends IWindowConfiguration { + windowId: number; + disableExtensions: boolean; data: IssueReporterData; features: IssueReporterFeatures; + os: { + type: string; + arch: string; + release: string; + }, + product: { + nameShort: string; + version: string; + commit: string | undefined; + date: string | undefined; + reportIssueUrl: string | undefined; + } } export function startup(configuration: IssueReporterConfiguration) { @@ -66,10 +65,7 @@ export function startup(configuration: IssueReporterConfiguration) { } export class IssueReporter extends Disposable { - private environmentService!: INativeEnvironmentService; private electronService!: IElectronService; - private telemetryService!: ITelemetryService; - private logService!: ILogService; private readonly issueReporterModel: IssueReporterModel; private numberOfSearchResultsDisplayed = 0; private receivedSystemInfo = false; @@ -79,7 +75,7 @@ export class IssueReporter extends Disposable { private readonly previewButton!: Button; - constructor(configuration: IssueReporterConfiguration) { + constructor(private readonly configuration: IssueReporterConfiguration) { super(); this.initServices(configuration); @@ -90,10 +86,10 @@ export class IssueReporter extends Disposable { this.issueReporterModel = new IssueReporterModel({ issueType: configuration.data.issueType || IssueType.Bug, versionInfo: { - vscodeVersion: `${product.nameShort} ${product.version} (${product.commit || 'Commit unknown'}, ${product.date || 'Date unknown'})`, - os: `${os.type()} ${os.arch()} ${os.release()}${isSnap ? ' snap' : ''}` + vscodeVersion: `${configuration.product.nameShort} ${configuration.product.version} (${configuration.product.commit || 'Commit unknown'}, ${configuration.product.date || 'Date unknown'})`, + os: `${this.configuration.os.type} ${this.configuration.os.arch} ${this.configuration.os.release}${isSnap ? ' snap' : ''}` }, - extensionsDisabled: !!this.environmentService.disableExtensions, + extensionsDisabled: !!configuration.disableExtensions, fileOnExtension: configuration.data.extensionId ? !targetExtension?.isBuiltin : undefined, selectedExtension: targetExtension, }); @@ -121,7 +117,6 @@ export class IssueReporter extends Disposable { } ipcRenderer.on('vscode:issuePerformanceInfoResponse', (_: unknown, info: Partial) => { - this.logService.trace('issueReporter: Received performance data'); this.issueReporterModel.update(info); this.receivedPerformanceInfo = true; @@ -132,7 +127,6 @@ export class IssueReporter extends Disposable { }); ipcRenderer.on('vscode:issueSystemInfoResponse', (_: unknown, info: SystemInfo) => { - this.logService.trace('issueReporter: Received system data'); this.issueReporterModel.update({ systemInfo: info }); this.receivedSystemInfo = true; @@ -144,7 +138,6 @@ export class IssueReporter extends Disposable { if (configuration.data.issueType === IssueType.PerformanceIssue) { ipcRenderer.send('vscode:issuePerformanceInfoRequest'); } - this.logService.trace('issueReporter: Sent data requests'); if (window.document.documentElement.lang !== 'en') { show(this.getElementById('english')); @@ -266,7 +259,7 @@ export class IssueReporter extends Disposable { this.issueReporterModel.update({ numberOfThemeExtesions, enabledNonThemeExtesions: nonThemes, allExtensions: installedExtensions }); this.updateExtensionTable(nonThemes, numberOfThemeExtesions); - if (this.environmentService.disableExtensions || installedExtensions.length === 0) { + if (this.configuration.disableExtensions || installedExtensions.length === 0) { (this.getElementById('disableExtensions')).disabled = true; } @@ -314,40 +307,13 @@ export class IssueReporter extends Disposable { } } - private initServices(configuration: INativeWindowConfiguration): void { + private initServices(configuration: IssueReporterConfiguration): void { const serviceCollection = new ServiceCollection(); const mainProcessService = new MainProcessService(configuration.windowId); serviceCollection.set(IMainProcessService, mainProcessService); this.electronService = new ElectronService(configuration.windowId, mainProcessService) as IElectronService; serviceCollection.set(IElectronService, this.electronService); - - this.environmentService = new EnvironmentService(configuration, configuration.execPath); - - const logService = new SpdLogService(`issuereporter${configuration.windowId}`, this.environmentService.logsPath, getLogLevel(this.environmentService)); - const loggerClient = new LoggerChannelClient(mainProcessService.getChannel('logger')); - this.logService = new FollowerLogService(loggerClient, logService); - - const sharedProcessService = createChannelSender(mainProcessService.getChannel('sharedProcess')); - - const sharedProcess = sharedProcessService.whenSharedProcessReady() - .then(() => connectNet(this.environmentService.sharedIPCHandle, `window:${configuration.windowId}`)); - - const instantiationService = new InstantiationService(serviceCollection, true); - if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { - const channel = getDelayedChannel(sharedProcess.then(c => c.getChannel('telemetryAppender'))); - const appender = combinedAppender(new TelemetryAppenderClient(channel), new LogAppender(logService)); - const commonProperties = resolveCommonProperties(product.commit || 'Commit unknown', product.version, configuration.machineId, product.msftInternalDomains, this.environmentService.installSourcePath); - const piiPaths = this.environmentService.extensionsPath ? [this.environmentService.appRoot, this.environmentService.extensionsPath] : [this.environmentService.appRoot]; - const config: ITelemetryServiceConfig = { appender, commonProperties, piiPaths, sendErrorTelemetry: true }; - - const telemetryService = instantiationService.createInstance(TelemetryService, config); - this._register(telemetryService); - - this.telemetryService = telemetryService; - } else { - this.telemetryService = NullTelemetryService; - } } private setEventHandlers(): void { @@ -617,11 +583,11 @@ export class IssueReporter extends Disposable { }, timeToWait * 1000); } } - }).catch(e => { - this.logSearchError(e); + }).catch(_ => { + // Ignore }); - }).catch(e => { - this.logSearchError(e); + }).catch(_ => { + // Ignore }); } @@ -648,11 +614,11 @@ export class IssueReporter extends Disposable { } else { throw new Error('Unexpected response, no candidates property'); } - }).catch((error) => { - this.logSearchError(error); + }).catch(_ => { + // Ignore }); - }).catch((error) => { - this.logSearchError(error); + }).catch(_ => { + // Ignore }); } @@ -705,18 +671,6 @@ export class IssueReporter extends Disposable { } } - private logSearchError(error: Error) { - this.logService.warn('issueReporter#search ', error.message); - type IssueReporterSearchErrorClassification = { - message: { classification: 'CallstackOrException', purpose: 'PerformanceAndHealth' } - }; - - type IssueReporterSearchError = { - message: string; - }; - this.telemetryService.publicLogError2('issueReporterSearchError', { message: error.message }); - } - private setUpTypes(): void { const makeOption = (issueType: IssueType, description: string) => ``; @@ -910,15 +864,6 @@ export class IssueReporter extends Disposable { return false; } - type IssueReporterSubmitClassification = { - issueType: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; - numSimilarIssuesDisplayed: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; - }; - type IssueReporterSubmitEvent = { - issueType: any; - numSimilarIssuesDisplayed: number; - }; - this.telemetryService.publicLog2('issueReporterSubmit', { issueType: this.issueReporterModel.getData().issueType, numSimilarIssuesDisplayed: this.numberOfSearchResultsDisplayed }); this.hasBeenSubmitted = true; const baseUrl = this.getIssueUrlWithTitle((this.getElementById('issue-title')).value); @@ -967,7 +912,7 @@ export class IssueReporter extends Disposable { } private getIssueUrlWithTitle(issueTitle: string): string { - let repositoryUrl = product.reportIssueUrl; + let repositoryUrl = this.configuration.product.reportIssueUrl; if (this.issueReporterModel.fileOnExtension()) { const extensionGitHubUrl = this.getExtensionGitHubUrl(); if (extensionGitHubUrl) { @@ -975,7 +920,7 @@ export class IssueReporter extends Disposable { } } - const queryStringPrefix = product.reportIssueUrl && product.reportIssueUrl.indexOf('?') === -1 ? '?' : '&'; + const queryStringPrefix = this.configuration.product.reportIssueUrl && this.configuration.product.reportIssueUrl.indexOf('?') === -1 ? '?' : '&'; return `${repositoryUrl}${queryStringPrefix}title=${encodeURIComponent(issueTitle)}`; } @@ -1136,7 +1081,7 @@ export class IssueReporter extends Disposable { private updateExtensionTable(extensions: IssueReporterExtensionData[], numThemeExtensions: number): void { const target = document.querySelector('.block-extensions .block-info'); if (target) { - if (this.environmentService.disableExtensions) { + if (this.configuration.disableExtensions) { target.innerHTML = localize('disabledExtensions', "Extensions are disabled"); return; } @@ -1193,7 +1138,6 @@ export class IssueReporter extends Disposable { // Exclude right click if (event.which < 3) { windowOpenNoOpener((event.target).href); - this.telemetryService.publicLog2('issueReporterViewSimilarIssue'); } } diff --git a/src/vs/code/electron-browser/issue/issueReporterModel.ts b/src/vs/code/electron-sandbox/issue/issueReporterModel.ts similarity index 97% rename from src/vs/code/electron-browser/issue/issueReporterModel.ts rename to src/vs/code/electron-sandbox/issue/issueReporterModel.ts index 47059817368..08bb22994a9 100644 --- a/src/vs/code/electron-browser/issue/issueReporterModel.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterModel.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { assign } from 'vs/base/common/objects'; import { IssueType, ISettingSearchResult, IssueReporterExtensionData } from 'vs/platform/issue/common/issue'; import { SystemInfo, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; @@ -49,7 +48,7 @@ export class IssueReporterModel { allExtensions: [] }; - this._data = initialData ? assign(defaultData, initialData) : defaultData; + this._data = initialData ? Object.assign(defaultData, initialData) : defaultData; } getData(): IssueReporterData { @@ -57,7 +56,7 @@ export class IssueReporterModel { } update(newData: Partial): void { - assign(this._data, newData); + Object.assign(this._data, newData); } serialize(): string { diff --git a/src/vs/code/electron-browser/issue/issueReporterPage.ts b/src/vs/code/electron-sandbox/issue/issueReporterPage.ts similarity index 100% rename from src/vs/code/electron-browser/issue/issueReporterPage.ts rename to src/vs/code/electron-sandbox/issue/issueReporterPage.ts diff --git a/src/vs/code/electron-browser/issue/media/issueReporter.css b/src/vs/code/electron-sandbox/issue/media/issueReporter.css similarity index 100% rename from src/vs/code/electron-browser/issue/media/issueReporter.css rename to src/vs/code/electron-sandbox/issue/media/issueReporter.css diff --git a/src/vs/code/electron-browser/issue/test/testReporterModel.test.ts b/src/vs/code/electron-sandbox/issue/test/testReporterModel.test.ts similarity index 98% rename from src/vs/code/electron-browser/issue/test/testReporterModel.test.ts rename to src/vs/code/electron-sandbox/issue/test/testReporterModel.test.ts index e7ce5b7e463..4d31af3dde7 100644 --- a/src/vs/code/electron-browser/issue/test/testReporterModel.test.ts +++ b/src/vs/code/electron-sandbox/issue/test/testReporterModel.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IssueReporterModel } from 'vs/code/electron-browser/issue/issueReporterModel'; +import { IssueReporterModel } from 'vs/code/electron-sandbox/issue/issueReporterModel'; import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; import { IssueType } from 'vs/platform/issue/common/issue'; diff --git a/src/vs/code/electron-browser/processExplorer/media/collapsed.svg b/src/vs/code/electron-sandbox/processExplorer/media/collapsed.svg similarity index 100% rename from src/vs/code/electron-browser/processExplorer/media/collapsed.svg rename to src/vs/code/electron-sandbox/processExplorer/media/collapsed.svg diff --git a/src/vs/code/electron-browser/processExplorer/media/expanded.svg b/src/vs/code/electron-sandbox/processExplorer/media/expanded.svg similarity index 100% rename from src/vs/code/electron-browser/processExplorer/media/expanded.svg rename to src/vs/code/electron-sandbox/processExplorer/media/expanded.svg diff --git a/src/vs/code/electron-browser/processExplorer/media/processExplorer.css b/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css similarity index 100% rename from src/vs/code/electron-browser/processExplorer/media/processExplorer.css rename to src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css diff --git a/src/vs/code/electron-browser/processExplorer/processExplorer.html b/src/vs/code/electron-sandbox/processExplorer/processExplorer.html similarity index 100% rename from src/vs/code/electron-browser/processExplorer/processExplorer.html rename to src/vs/code/electron-sandbox/processExplorer/processExplorer.html diff --git a/src/vs/code/electron-browser/processExplorer/processExplorer.js b/src/vs/code/electron-sandbox/processExplorer/processExplorer.js similarity index 84% rename from src/vs/code/electron-browser/processExplorer/processExplorer.js rename to src/vs/code/electron-sandbox/processExplorer/processExplorer.js index 97faa434a26..d6c6886e6fd 100644 --- a/src/vs/code/electron-browser/processExplorer/processExplorer.js +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer.js @@ -14,6 +14,6 @@ const bootstrapWindow = (() => { return window.MonacoBootstrapWindow; })(); -bootstrapWindow.load(['vs/code/electron-browser/processExplorer/processExplorerMain'], function (processExplorer, configuration) { - processExplorer.startup(configuration.data); +bootstrapWindow.load(['vs/code/electron-sandbox/processExplorer/processExplorerMain'], function (processExplorer, configuration) { + processExplorer.startup(configuration.windowId, configuration.data); }, { forceEnableDeveloperKeybindings: true }); diff --git a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts b/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts similarity index 85% rename from src/vs/code/electron-browser/processExplorer/processExplorerMain.ts rename to src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts index 59787a5a365..3c88a619a55 100644 --- a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts @@ -4,20 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/processExplorer'; -import { clipboard } from 'electron'; -import { totalmem } from 'os'; +import { ElectronService, IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; -import product from 'vs/platform/product/common/product'; import { localize } from 'vs/nls'; import { ProcessExplorerStyles, ProcessExplorerData } from 'vs/platform/issue/common/issue'; import { applyZoom, zoomIn, zoomOut } from 'vs/platform/windows/electron-sandbox/window'; -import * as platform from 'vs/base/common/platform'; import { IContextMenuItem } from 'vs/base/parts/contextmenu/common/contextmenu'; import { popup } from 'vs/base/parts/contextmenu/electron-sandbox/contextmenu'; import { ProcessItem } from 'vs/base/common/processes'; import { addDisposableListener, addClass } from 'vs/base/browser/dom'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { isRemoteDiagnosticError, IRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; +import { MainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; const DEBUG_FLAGS_PATTERN = /\s--(inspect|debug)(-brk|port)?=(\d+)?/; const DEBUG_PORT_PATTERN = /\s--(inspect|debug)-port=(\d+)/; @@ -40,7 +38,12 @@ class ProcessExplorer { private listeners = new DisposableStore(); - constructor(data: ProcessExplorerData) { + private electronService: IElectronService; + + constructor(windowId: number, private data: ProcessExplorerData) { + const mainProcessService = new MainProcessService(windowId); + this.electronService = new ElectronService(windowId, mainProcessService) as IElectronService; + this.applyStyles(data.styles); // Map window process pids to titles, annotate process names with this when rendering to distinguish between them @@ -59,24 +62,24 @@ class ProcessExplorer { ipcRenderer.send('vscode:listProcesses'); } - private getProcessList(rootProcess: ProcessItem, isLocal: boolean): FormattedProcessItem[] { + private getProcessList(rootProcess: ProcessItem, isLocal: boolean, totalMem: number): FormattedProcessItem[] { const processes: FormattedProcessItem[] = []; if (rootProcess) { - this.getProcessItem(processes, rootProcess, 0, isLocal); + this.getProcessItem(processes, rootProcess, 0, isLocal, totalMem); } return processes; } - private getProcessItem(processes: FormattedProcessItem[], item: ProcessItem, indent: number, isLocal: boolean): void { + private getProcessItem(processes: FormattedProcessItem[], item: ProcessItem, indent: number, isLocal: boolean, totalMem: number): void { const isRoot = (indent === 0); const MB = 1024 * 1024; let name = item.name; if (isRoot) { - name = isLocal ? `${product.applicationName} main` : 'remote agent'; + name = isLocal ? `${this.data.applicationName} main` : 'remote agent'; } if (name === 'window') { @@ -86,7 +89,7 @@ class ProcessExplorer { // Format name with indent const formattedName = isRoot ? name : `${' '.repeat(indent)} ${name}`; - const memory = process.platform === 'win32' ? item.mem : (totalmem() * (item.mem / 100)); + const memory = this.data.platform === 'win32' ? item.mem : (totalMem * (item.mem / 100)); processes.push({ cpu: item.load, memory: (memory / MB), @@ -100,7 +103,7 @@ class ProcessExplorer { if (Array.isArray(item.children)) { item.children.forEach(child => { if (child) { - this.getProcessItem(processes, child, indent + 1, isLocal); + this.getProcessItem(processes, child, indent + 1, isLocal, totalMem); } }); } @@ -258,7 +261,7 @@ class ProcessExplorer { container.appendChild(body); } - private updateProcessInfo(processLists: [{ name: string, rootProcess: ProcessItem | IRemoteDiagnosticError }]): void { + private async updateProcessInfo(processLists: [{ name: string, rootProcess: ProcessItem | IRemoteDiagnosticError }]): Promise { const container = document.getElementById('process-list'); if (!container) { return; @@ -271,19 +274,20 @@ class ProcessExplorer { tableHead.innerHTML = ` ${localize('cpu', "CPU %")} ${localize('memory', "Memory (MB)")} - ${localize('pid', "pid")} + ${localize('pid', "PID")} ${localize('name', "Name")} `; container.append(tableHead); const hasMultipleMachines = Object.keys(processLists).length > 1; + const totalMem = await this.electronService.getTotalMem(); processLists.forEach((remote, i) => { const isLocal = i === 0; if (isRemoteDiagnosticError(remote.rootProcess)) { this.renderProcessFetchError(remote.name, remote.rootProcess.errorMessage); } else { - this.renderTableSection(remote.name, this.getProcessList(remote.rootProcess, isLocal), hasMultipleMachines, isLocal); + this.renderTableSection(remote.name, this.getProcessList(remote.rootProcess, isLocal, totalMem), hasMultipleMachines, isLocal); } }); } @@ -322,15 +326,15 @@ class ProcessExplorer { if (isLocal) { items.push({ label: localize('killProcess', "Kill Process"), - click() { - process.kill(pid, 'SIGTERM'); + click: () => { + this.electronService.killProcess(pid, 'SIGTERM'); } }); items.push({ label: localize('forceKillProcess', "Force Kill Process"), - click() { - process.kill(pid, 'SIGKILL'); + click: () => { + this.electronService.killProcess(pid, 'SIGKILL'); } }); @@ -341,20 +345,20 @@ class ProcessExplorer { items.push({ label: localize('copy', "Copy"), - click() { + click: () => { const row = document.getElementById(pid.toString()); if (row) { - clipboard.writeText(row.innerText); + this.electronService.writeClipboardText(row.innerText); } } }); items.push({ label: localize('copyAll', "Copy All"), - click() { + click: () => { const processList = document.getElementById('process-list'); if (processList) { - clipboard.writeText(processList.innerText); + this.electronService.writeClipboardText(processList.innerText); } } }); @@ -398,15 +402,15 @@ class ProcessExplorer { -export function startup(data: ProcessExplorerData): void { - const platformClass = platform.isWindows ? 'windows' : platform.isLinux ? 'linux' : 'mac'; +export function startup(windowId: number, data: ProcessExplorerData): void { + const platformClass = data.platform === 'win32' ? 'windows' : data.platform === 'linux' ? 'linux' : 'mac'; addClass(document.body, platformClass); // used by our fonts applyZoom(data.zoomLevel); - const processExplorer = new ProcessExplorer(data); + const processExplorer = new ProcessExplorer(windowId, data); document.onkeydown = (e: KeyboardEvent) => { - const cmdOrCtrlKey = platform.isMacintosh ? e.metaKey : e.ctrlKey; + const cmdOrCtrlKey = data.platform === 'darwin' ? e.metaKey : e.ctrlKey; // Cmd/Ctrl + zooms in if (cmdOrCtrlKey && e.keyCode === 187) { @@ -421,7 +425,7 @@ export function startup(data: ProcessExplorerData): void { // Cmd/Ctrl + w closes process explorer window.addEventListener('keydown', e => { - const cmdOrCtrlKey = platform.isMacintosh ? e.metaKey : e.ctrlKey; + const cmdOrCtrlKey = data.platform === 'darwin' ? e.metaKey : e.ctrlKey; if (cmdOrCtrlKey && e.keyCode === 87) { processExplorer.dispose(); ipcRenderer.send('vscode:closeProcessExplorer'); diff --git a/src/vs/editor/browser/editorExtensions.ts b/src/vs/editor/browser/editorExtensions.ts index 1db05bd2a7c..e4e081bd300 100644 --- a/src/vs/editor/browser/editorExtensions.ts +++ b/src/vs/editor/browser/editorExtensions.ts @@ -337,6 +337,44 @@ export abstract class EditorAction extends EditorCommand { public abstract run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise; } +export abstract class MultiEditorAction extends EditorAction { + private readonly _implementations: [number, CommandImplementation][] = []; + + constructor(opts: IActionOptions) { + super(opts); + } + + public addImplementation(priority: number, implementation: CommandImplementation): IDisposable { + this._implementations.push([priority, implementation]); + this._implementations.sort((a, b) => b[0] - a[0]); + return { + dispose: () => { + for (let i = 0; i < this._implementations.length; i++) { + if (this._implementations[i][1] === implementation) { + this._implementations.splice(i, 1); + return; + } + } + } + }; + } + + public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise { + this.reportTelemetry(accessor, editor); + + for (const impl of this._implementations) { + if (impl[1](accessor, args)) { + return; + } + } + + return this.run(accessor, editor, args || {}); + } + + public abstract run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise; + +} + //#endregion EditorAction //#region EditorAction2 @@ -474,6 +512,11 @@ export function registerEditorAction(ctor: { new(): T; } return action; } +export function registerMultiEditorAction(action: T): T { + EditorContributionRegistry.INSTANCE.registerEditorAction(action); + return action; +} + export function registerInstantiatedEditorAction(editorAction: EditorAction): void { EditorContributionRegistry.INSTANCE.registerEditorAction(editorAction); } diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index 121365ddbc6..3bde5dfd839 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -381,7 +381,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC } continue; } - if (typeof baseValue === 'object' && typeof subsetValue === 'object') { + if (baseValue && typeof baseValue === 'object' && subsetValue && typeof subsetValue === 'object') { if (!this._subsetEquals(baseValue, subsetValue)) { return false; } diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index a366e265674..6176dcd8fa0 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -2572,9 +2572,9 @@ class EditorPixelRatio extends ComputedEditorOption>; diff --git a/src/vs/editor/common/modes/modesRegistry.ts b/src/vs/editor/common/modes/modesRegistry.ts index f5c523e6574..c2ef63388de 100644 --- a/src/vs/editor/common/modes/modesRegistry.ts +++ b/src/vs/editor/common/modes/modesRegistry.ts @@ -62,7 +62,7 @@ export const PLAINTEXT_LANGUAGE_IDENTIFIER = new LanguageIdentifier(PLAINTEXT_MO ModesRegistry.registerLanguage({ id: PLAINTEXT_MODE_ID, - extensions: ['.txt', '.gitignore'], + extensions: ['.txt'], aliases: [nls.localize('plainText.alias', "Plain Text"), 'text'], mimetypes: ['text/plain'] }); diff --git a/src/vs/editor/common/services/languagesRegistry.ts b/src/vs/editor/common/services/languagesRegistry.ts index 3275621b978..c734735a733 100644 --- a/src/vs/editor/common/services/languagesRegistry.ts +++ b/src/vs/editor/common/services/languagesRegistry.ts @@ -153,9 +153,14 @@ export class LanguagesRegistry extends Disposable { } if (Array.isArray(lang.extensions)) { + if (lang.configuration) { + // insert first as this appears to be the 'primary' language definition + resolvedLanguage.extensions = lang.extensions.concat(resolvedLanguage.extensions); + } else { + resolvedLanguage.extensions = resolvedLanguage.extensions.concat(lang.extensions); + } for (let extension of lang.extensions) { mime.registerTextMime({ id: langId, mime: primaryMime, extension: extension }, this._warnOnOverwrite); - resolvedLanguage.extensions.push(extension); } } diff --git a/src/vs/editor/contrib/codeAction/codeActionMenu.ts b/src/vs/editor/contrib/codeAction/codeActionMenu.ts index b736d9c2884..0c9ef9ec71a 100644 --- a/src/vs/editor/contrib/codeAction/codeActionMenu.ts +++ b/src/vs/editor/contrib/codeAction/codeActionMenu.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { getDomNodePagePosition } from 'vs/base/browser/dom'; -import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IAnchor } from 'vs/base/browser/ui/contextview/contextview'; -import { Action, IAction } from 'vs/base/common/actions'; +import { Action, IAction, Separator } from 'vs/base/common/actions'; import { canceled } from 'vs/base/common/errors'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; @@ -90,6 +89,7 @@ export class CodeActionMenu extends Disposable { const resolver = this._keybindingResolver.getResolver(); this._contextMenuService.showContextMenu({ + domForShadowRoot: this._editor.getDomNode()!, getAnchor: () => anchor, getActions: () => menuActions, onHide: () => { diff --git a/src/vs/editor/contrib/contextmenu/contextmenu.ts b/src/vs/editor/contrib/contextmenu/contextmenu.ts index 6114c170cce..8a2a53b8dde 100644 --- a/src/vs/editor/contrib/contextmenu/contextmenu.ts +++ b/src/vs/editor/contrib/contextmenu/contextmenu.ts @@ -6,9 +6,8 @@ import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { ActionViewItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IAnchor } from 'vs/base/browser/ui/contextview/contextview'; -import { IAction } from 'vs/base/common/actions'; +import { IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; import { KeyCode, KeyMod, ResolvedKeybinding } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; @@ -23,7 +22,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { ITextModel } from 'vs/editor/common/model'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { ContextSubMenu } from 'vs/base/browser/contextmenu'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; export class ContextMenuController implements IEditorContribution { @@ -50,7 +49,14 @@ export class ContextMenuController implements IEditorContribution { this._toDispose.add(this._editor.onContextMenu((e: IEditorMouseEvent) => this._onContextMenu(e))); this._toDispose.add(this._editor.onMouseWheel((e: IMouseWheelEvent) => { if (this._contextMenuIsBeingShownCount > 0) { - this._contextViewService.hideContextView(); + const view = this._contextViewService.getContextViewElement(); + const target = e.srcElement as HTMLElement; + + // Event triggers on shadow root host first + // Check if the context view is under this host before hiding it #103169 + if (!(target.shadowRoot && dom.getShadowRoot(view) === target.shadowRoot)) { + this._contextViewService.hideContextView(); + } } })); this._toDispose.add(this._editor.onKeyDown((e: IKeyboardEvent) => { @@ -153,7 +159,7 @@ export class ContextMenuController implements IEditorContribution { if (action instanceof SubmenuItemAction) { const subActions = this._getMenuActions(model, action.item.submenu); if (subActions.length > 0) { - result.push(new ContextSubMenu(action.label, subActions)); + result.push(new SubmenuAction(action.id, action.label, subActions)); addedItems++; } } else { @@ -174,7 +180,7 @@ export class ContextMenuController implements IEditorContribution { return result; } - private _doShowContextMenu(actions: ReadonlyArray, anchor: IAnchor | null = null): void { + private _doShowContextMenu(actions: IAction[], anchor: IAnchor | null = null): void { if (!this._editor.hasModel()) { return; } @@ -205,6 +211,8 @@ export class ContextMenuController implements IEditorContribution { // Show menu this._contextMenuIsBeingShownCount++; this._contextMenuService.showContextMenu({ + domForShadowRoot: this._editor.getDomNode(), + getAnchor: () => anchor!, getActions: () => actions, diff --git a/src/vs/editor/contrib/dnd/dnd.ts b/src/vs/editor/contrib/dnd/dnd.ts index 599e804eb05..b4bdd4336fd 100644 --- a/src/vs/editor/contrib/dnd/dnd.ts +++ b/src/vs/editor/contrib/dnd/dnd.ts @@ -54,6 +54,7 @@ export class DragAndDropController extends Disposable implements IEditorContribu this._register(this._editor.onKeyDown((e: IKeyboardEvent) => this.onEditorKeyDown(e))); this._register(this._editor.onKeyUp((e: IKeyboardEvent) => this.onEditorKeyUp(e))); this._register(this._editor.onDidBlurEditorWidget(() => this.onEditorBlur())); + this._register(this._editor.onDidBlurEditorText(() => this.onEditorBlur())); this._dndDecorationIds = []; this._mouseDown = false; this._modifierPressed = false; diff --git a/src/vs/editor/contrib/find/findController.ts b/src/vs/editor/contrib/find/findController.ts index 712501a3933..2730f6b8040 100644 --- a/src/vs/editor/contrib/find/findController.ts +++ b/src/vs/editor/contrib/find/findController.ts @@ -9,7 +9,7 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, EditorCommand, ServicesAccessor, registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { EditorAction, EditorCommand, ServicesAccessor, registerEditorAction, registerEditorCommand, registerEditorContribution, MultiEditorAction, registerMultiEditorAction } from 'vs/editor/browser/editorExtensions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { CONTEXT_FIND_INPUT_FOCUSED, CONTEXT_FIND_WIDGET_VISIBLE, FIND_IDS, FindModelBoundToEditorModel, ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleSearchScopeKeybinding, ToggleWholeWordKeybinding, CONTEXT_REPLACE_INPUT_FOCUSED } from 'vs/editor/contrib/find/findModel'; @@ -457,7 +457,7 @@ export class FindController extends CommonFindController implements IFindControl } } -export class StartFindAction extends EditorAction { +export class StartFindAction extends MultiEditorAction { constructor() { super({ @@ -706,7 +706,7 @@ export class PreviousSelectionMatchFindAction extends SelectionMatchFindAction { } } -export class StartFindReplaceAction extends EditorAction { +export class StartFindReplaceAction extends MultiEditorAction { constructor() { super({ @@ -769,7 +769,8 @@ export class StartFindReplaceAction extends EditorAction { registerEditorContribution(CommonFindController.ID, FindController); -registerEditorAction(StartFindAction); +export const EditorStartFindAction = new StartFindAction(); +registerMultiEditorAction(EditorStartFindAction); registerEditorAction(StartFindWithSelectionAction); registerEditorAction(NextMatchFindAction); registerEditorAction(NextMatchFindAction2); @@ -777,7 +778,8 @@ registerEditorAction(PreviousMatchFindAction); registerEditorAction(PreviousMatchFindAction2); registerEditorAction(NextSelectionMatchFindAction); registerEditorAction(PreviousSelectionMatchFindAction); -registerEditorAction(StartFindReplaceAction); +export const EditorStartFindReplaceAction = new StartFindReplaceAction(); +registerMultiEditorAction(EditorStartFindReplaceAction); const FindCommand = EditorCommand.bindToContribution(CommonFindController.get); diff --git a/src/vs/editor/contrib/find/findWidget.css b/src/vs/editor/contrib/find/findWidget.css index dfe22ad235a..303f435450a 100644 --- a/src/vs/editor/contrib/find/findWidget.css +++ b/src/vs/editor/contrib/find/findWidget.css @@ -6,7 +6,7 @@ /* Find widget */ .monaco-editor .find-widget { position: absolute; - z-index: 10; + z-index: 20; height: 33px; overflow: hidden; line-height: 19px; diff --git a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts index 7c2eed05550..01a7dc55aa0 100644 --- a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts @@ -26,9 +26,9 @@ import { IActionBarOptions, ActionsOrientation } from 'vs/base/browser/ui/action import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { MenuId, IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { MenuId, IMenuService } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { createAndFillInActionBarActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; class MessageWidget { @@ -246,10 +246,10 @@ export class MarkerNavigationWidget extends PeekViewWidget { @IThemeService private readonly _themeService: IThemeService, @IOpenerService private readonly _openerService: IOpenerService, @IMenuService private readonly _menuService: IMenuService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService ) { - super(editor, { showArrow: true, showFrame: true, isAccessible: true }); + super(editor, { showArrow: true, showFrame: true, isAccessible: true }, instantiationService); this._severity = MarkerSeverity.Warning; this._backgroundColor = Color.white; @@ -311,8 +311,8 @@ export class MarkerNavigationWidget extends PeekViewWidget { protected _getActionBarOptions(): IActionBarOptions { return { - orientation: ActionsOrientation.HORIZONTAL, - actionViewItemProvider: action => action instanceof MenuItemAction ? this._instantiationService.createInstance(MenuEntryActionViewItem, action) : undefined + ...super._getActionBarOptions(), + orientation: ActionsOrientation.HORIZONTAL }; } diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts index 021c0dd6836..f1bbff0aa81 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts @@ -216,6 +216,15 @@ export abstract class ReferencesController implements IEditorContribution { } } + async revealReference(reference: OneReference): Promise { + if (!this._editor.hasModel() || !this._model || !this._widget) { + // can be called while still resolving... + return; + } + + await this._widget.revealReference(reference); + } + closeWidget(focusEditor = true): void { dispose(this._widget); dispose(this._model); @@ -365,6 +374,24 @@ KeybindingsRegistry.registerKeybindingRule({ }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'revealReference', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Enter, + mac: { + primary: KeyCode.Enter, + secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow] + }, + when: ContextKeyExpr.and(ctxReferenceSearchVisible, WorkbenchListFocusContextKey), + handler(accessor: ServicesAccessor) { + const listService = accessor.get(IListService); + const focus = listService.lastFocusedList?.getFocus(); + if (Array.isArray(focus) && focus[0] instanceof OneReference) { + withController(accessor, controller => controller.revealReference(focus[0])); + } + } +}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'openReferenceToSide', weight: KeybindingWeight.EditorContrib, diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts index 49fa98ec1cd..7d429358a35 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts @@ -220,7 +220,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { @ILabelService private readonly _uriLabel: ILabelService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, ) { - super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true }); + super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true }, _instantiationService); this._applyTheme(themeService.getColorTheme()); this._callOnDispose.add(themeService.onDidColorThemeChange(this._applyTheme.bind(this))); @@ -481,6 +481,11 @@ export class ReferenceWidget extends peekView.PeekViewWidget { return undefined; } + async revealReference(reference: OneReference): Promise { + await this._revealReference(reference, false); + this._onDidSelectReference.fire({ element: reference, kind: 'goto', source: 'tree' }); + } + private _revealedReference?: OneReference; private async _revealReference(reference: OneReference, revealParent: boolean): Promise { diff --git a/src/vs/editor/contrib/hover/hover.ts b/src/vs/editor/contrib/hover/hover.ts index cb1f9a7a553..01b4416ffc5 100644 --- a/src/vs/editor/contrib/hover/hover.ts +++ b/src/vs/editor/contrib/hover/hover.ts @@ -101,6 +101,7 @@ export class ModesHoverController implements IEditorContribution { this._toUnhook.add(this._editor.onDidChangeModelDecorations(() => this._onModelDecorationsChanged())); } else { this._toUnhook.add(this._editor.onMouseMove(hideWidgetsEventHandler)); + this._toUnhook.add(this._editor.onKeyDown((e: IKeyboardEvent) => this._onKeyDown(e))); } this._toUnhook.add(this._editor.onMouseLeave(hideWidgetsEventHandler)); diff --git a/src/vs/editor/contrib/peekView/peekView.ts b/src/vs/editor/contrib/peekView/peekView.ts index 6a5bd94648f..cbada7a1e48 100644 --- a/src/vs/editor/contrib/peekView/peekView.ts +++ b/src/vs/editor/contrib/peekView/peekView.ts @@ -18,7 +18,7 @@ import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeE import { IOptions, IStyles, ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget'; import * as nls from 'vs/nls'; import { RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ServicesAccessor, createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ServicesAccessor, createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable } from 'vs/base/common/lifecycle'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -26,7 +26,8 @@ import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { registerColor, contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { Codicon } from 'vs/base/common/codicons'; - +import { MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; export const IPeekViewService = createDecorator('IPeekViewService'); export interface IPeekViewService { @@ -115,7 +116,11 @@ export abstract class PeekViewWidget extends ZoneWidget { protected _actionbarWidget?: ActionBar; protected _bodyElement?: HTMLDivElement; - constructor(editor: ICodeEditor, options: IPeekViewOptions = {}) { + constructor( + editor: ICodeEditor, + options: IPeekViewOptions, + @IInstantiationService protected readonly instantiationService: IInstantiationService + ) { super(editor, options); objects.mixin(this.options, defaultOptions, false); } @@ -199,7 +204,17 @@ export abstract class PeekViewWidget extends ZoneWidget { } protected _getActionBarOptions(): IActionBarOptions { - return {}; + return { + actionViewItemProvider: action => { + if (action instanceof MenuItemAction) { + return this.instantiationService.createInstance(MenuEntryActionViewItem, action); + } else if (action instanceof SubmenuItemAction) { + return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); + } + + return undefined; + } + }; } protected _onTitleClick(event: IMouseEvent): void { diff --git a/src/vs/editor/contrib/rename/media/onTypeRename.css b/src/vs/editor/contrib/rename/media/onTypeRename.css index 2c80c5957b1..16bb0178528 100644 --- a/src/vs/editor/contrib/rename/media/onTypeRename.css +++ b/src/vs/editor/contrib/rename/media/onTypeRename.css @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ .monaco-editor .on-type-rename-decoration { - background: rgba(255, 0, 0, 0.3); - border-left: 1px solid rgba(255, 0, 0, 0.3); + border-left: 1px solid transparent; /* So border can be transparent */ background-clip: padding-box; } diff --git a/src/vs/editor/contrib/rename/onTypeRename.ts b/src/vs/editor/contrib/rename/onTypeRename.ts index a7c6fd064fe..51cc31bbefe 100644 --- a/src/vs/editor/contrib/rename/onTypeRename.ts +++ b/src/vs/editor/contrib/rename/onTypeRename.ts @@ -26,6 +26,9 @@ import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { 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'; export const CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE = new RawContextKey('onTypeRenameInputVisible', false); @@ -360,6 +363,13 @@ export function getOnTypeRenameRanges(model: ITextModel, position: Position, tok }), result => !!result && arrays.isNonEmptyArray(result?.ranges)); } +export const editorOnTypeRenameBackground = registerColor('editor.onTypeRenameBackground', { dark: Color.fromHex('#f00').transparent(0.3), light: Color.fromHex('#f00').transparent(0.3), hc: Color.fromHex('#f00').transparent(0.3) }, nls.localize('editorOnTypeRenameBackground', 'Background color when the editor auto renames on type.')); +registerThemingParticipant((theme, collector) => { + const editorOnTypeRenameBackgroundColor = theme.getColor(editorOnTypeRenameBackground); + if (editorOnTypeRenameBackgroundColor) { + collector.addRule(`.monaco-editor .on-type-rename-decoration { background: ${editorOnTypeRenameBackgroundColor}; border-left-color: ${editorOnTypeRenameBackgroundColor}; }`); + } +}); registerModelAndPositionCommand('_executeRenameOnTypeProvider', (model, position) => getOnTypeRenameRanges(model, position, CancellationToken.None)); diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 05758a08ba9..7b19fefb903 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -43,9 +43,10 @@ import { MarkdownString } from 'vs/base/common/htmlContent'; import { flatten, isFalsyOrEmpty } from 'vs/base/common/arrays'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMenuService } from 'vs/platform/actions/common/actions'; -import { ActionBar, IActionViewItemProvider, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IAction } from 'vs/base/common/actions'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IAction, IActionViewItemProvider } from 'vs/base/common/actions'; import { Codicon, registerIcon } from 'vs/base/common/codicons'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; const expandSuggestionDocsByDefault = false; @@ -617,7 +618,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate { return Promise.resolve(undefined); } diff --git a/src/vs/editor/test/common/config/commonEditorConfig.test.ts b/src/vs/editor/test/common/config/commonEditorConfig.test.ts index c0196b37bd1..d0edc8f44c0 100644 --- a/src/vs/editor/test/common/config/commonEditorConfig.test.ts +++ b/src/vs/editor/test/common/config/commonEditorConfig.test.ts @@ -211,4 +211,15 @@ suite('Common Editor Config', () => { strings: false }); }); + + test('issue #102920: Can\'t snap or split view with JSON files', () => { + const config = new TestConfiguration({ quickSuggestions: null! }); + config.updateOptions({ quickSuggestions: { strings: true } }); + const actual = >>config.options.get(EditorOption.quickSuggestions); + assert.deepEqual(actual, { + other: true, + comments: false, + strings: true + }); + }); }); diff --git a/src/vs/editor/test/common/model/benchmark/searchNReplace.benchmark.ts b/src/vs/editor/test/common/model/benchmark/searchNReplace.benchmark.ts index aecb8ad46ca..7a417a22aab 100644 --- a/src/vs/editor/test/common/model/benchmark/searchNReplace.benchmark.ts +++ b/src/vs/editor/test/common/model/benchmark/searchNReplace.benchmark.ts @@ -7,30 +7,30 @@ import { ITextBufferBuilder } from 'vs/editor/common/model'; import { BenchmarkSuite } from 'vs/editor/test/common/model/benchmark/benchmarkUtils'; import { generateRandomChunkWithLF, generateRandomReplaces } from 'vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils'; -let fileSizes = [1, 1000, 64 * 1000, 32 * 1000 * 1000]; +const fileSizes = [1, 1000, 64 * 1000, 32 * 1000 * 1000]; -for (let fileSize of fileSizes) { - let chunks: string[] = []; +for (const fileSize of fileSizes) { + const chunks: string[] = []; - let chunkCnt = Math.floor(fileSize / (64 * 1000)); + const chunkCnt = Math.floor(fileSize / (64 * 1000)); if (chunkCnt === 0) { chunks.push(generateRandomChunkWithLF(fileSize, fileSize)); } else { - let chunk = generateRandomChunkWithLF(64 * 1000, 64 * 1000); + const chunk = generateRandomChunkWithLF(64 * 1000, 64 * 1000); // try to avoid OOM for (let j = 0; j < chunkCnt; j++) { chunks.push(Buffer.from(chunk + j).toString()); } } - let replaceSuite = new BenchmarkSuite({ + const replaceSuite = new BenchmarkSuite({ name: `File Size: ${fileSize}Byte`, iterations: 10 }); - let edits = generateRandomReplaces(chunks, 500, 5, 10); + const edits = generateRandomReplaces(chunks, 500, 5, 10); - for (let i of [10, 100, 500]) { + for (const i of [10, 100, 500]) { replaceSuite.add({ name: `replace ${i} occurrences`, buildBuffer: (textBufferBuilder: ITextBufferBuilder) => { diff --git a/src/vs/editor/test/common/services/languagesRegistry.test.ts b/src/vs/editor/test/common/services/languagesRegistry.test.ts index 45c3ba17966..09ef74cd000 100644 --- a/src/vs/editor/test/common/services/languagesRegistry.test.ts +++ b/src/vs/editor/test/common/services/languagesRegistry.test.ts @@ -221,6 +221,32 @@ suite('LanguagesRegistry', () => { assert.deepEqual(registry.getExtensions('aName'), ['aExt', 'aExt2']); }); + test('extensions of primary language registration come first', () => { + let registry = new LanguagesRegistry(false); + + registry._registerLanguages([{ + id: 'a', + extensions: ['aExt3'] + }]); + + assert.deepEqual(registry.getExtensions('a')[0], 'aExt3'); + + registry._registerLanguages([{ + id: 'a', + configuration: URI.file('conf.json'), + extensions: ['aExt'] + }]); + + assert.deepEqual(registry.getExtensions('a')[0], 'aExt'); + + registry._registerLanguages([{ + id: 'a', + extensions: ['aExt2'] + }]); + + assert.deepEqual(registry.getExtensions('a')[0], 'aExt'); + }); + test('filenames', () => { let registry = new LanguagesRegistry(false); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index d410ae9a711..0593f65eb4a 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3557,9 +3557,9 @@ declare namespace monaco.editor { * Configuration options for quick suggestions */ export interface IQuickSuggestionsOptions { - other: boolean; - comments: boolean; - strings: boolean; + other?: boolean; + comments?: boolean; + strings?: boolean; } export type ValidQuickSuggestionsOptions = boolean | Readonly>; diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 4e3e1dec7a2..d0867eb9d8d 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -5,8 +5,7 @@ import { addClasses, createCSSRule, removeClasses, asCSSUrl } from 'vs/base/browser/dom'; import { domEvent } from 'vs/base/browser/event'; -import { ActionViewItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IAction } from 'vs/base/common/actions'; +import { IAction, Separator } from 'vs/base/common/actions'; import { Emitter } from 'vs/base/common/event'; import { IdGenerator } from 'vs/base/common/idGenerator'; import { IDisposable, toDisposable, MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -17,6 +16,8 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; // The alternative key on all platforms is alt. On windows we also support shift as an alternative key #44136 class AlternativeKeyEmitter extends Emitter { @@ -124,19 +125,11 @@ function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray(); - static readonly ICON_PATH_TO_CSS_RULES: Map = new Map(); +export class MenuEntryActionViewItem extends ActionViewItem { private _wantsAltCommand: boolean = false; private readonly _itemClassDispose = this._register(new MutableDisposable()); @@ -164,7 +157,7 @@ export class MenuEntryActionViewItem extends ActionViewItem { this._altKey.suppressAltKeyUp(); } - this.actionRunner.run(this._commandAction) + this.actionRunner.run(this._commandAction, this._context) .then(undefined, err => this._notificationService.error(err)); } @@ -235,7 +228,7 @@ export class MenuEntryActionViewItem extends ActionViewItem { } } - _updateItemClass(item: ICommandAction): void { + private _updateItemClass(item: ICommandAction): void { this._itemClassDispose.value = undefined; const icon = this._commandAction.checked && (item.toggled as { icon?: Icon })?.icon ? (item.toggled as { icon: Icon }).icon : item.icon; @@ -256,17 +249,17 @@ export class MenuEntryActionViewItem extends ActionViewItem { // icon path let iconClass: string; - if (icon?.dark?.scheme) { + if (icon.dark?.scheme) { const iconPathMapKey = icon.dark.toString(); - if (MenuEntryActionViewItem.ICON_PATH_TO_CSS_RULES.has(iconPathMapKey)) { - iconClass = MenuEntryActionViewItem.ICON_PATH_TO_CSS_RULES.get(iconPathMapKey)!; + if (ICON_PATH_TO_CSS_RULES.has(iconPathMapKey)) { + iconClass = ICON_PATH_TO_CSS_RULES.get(iconPathMapKey)!; } else { iconClass = ids.nextId(); createCSSRule(`.icon.${iconClass}`, `background-image: ${asCSSUrl(icon.light || icon.dark)}`); createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: ${asCSSUrl(icon.dark)}`); - MenuEntryActionViewItem.ICON_PATH_TO_CSS_RULES.set(iconPathMapKey, iconClass); + ICON_PATH_TO_CSS_RULES.set(iconPathMapKey, iconClass); } if (this.label) { @@ -283,16 +276,33 @@ export class MenuEntryActionViewItem extends ActionViewItem { } } -// Need to subclass MenuEntryActionViewItem in order to respect -// the action context coming from any action bar, without breaking -// existing users -export class ContextAwareMenuEntryActionViewItem extends MenuEntryActionViewItem { +export class SubmenuEntryActionViewItem extends DropdownMenuActionViewItem { - onClick(event: MouseEvent): void { - event.preventDefault(); - event.stopPropagation(); + constructor( + action: SubmenuItemAction, + @INotificationService _notificationService: INotificationService, + @IContextMenuService _contextMenuService: IContextMenuService + ) { + const classNames: string[] = []; - this.actionRunner.run(this._commandAction, this._context) - .then(undefined, err => this._notificationService.error(err)); + if (action.item.icon) { + if (ThemeIcon.isThemeIcon(action.item.icon)) { + classNames.push(ThemeIcon.asClassName(action.item.icon)!); + } else if (action.item.icon.dark?.scheme) { + const iconPathMapKey = action.item.icon.dark.toString(); + + if (ICON_PATH_TO_CSS_RULES.has(iconPathMapKey)) { + classNames.push('icon', ICON_PATH_TO_CSS_RULES.get(iconPathMapKey)!); + } else { + const className = ids.nextId(); + classNames.push('icon', className); + createCSSRule(`.icon.${className}`, `background-image: ${asCSSUrl(action.item.icon.light || action.item.icon.dark)}`); + createCSSRule(`.vs-dark .icon.${className}, .hc-black .icon.${className}`, `background-image: ${asCSSUrl(action.item.icon.dark)}`); + ICON_PATH_TO_CSS_RULES.set(iconPathMapKey, className); + } + } + } + + super(action, Array.isArray(action.actions) ? action.actions : action.actions(), _contextMenuService, { classNames }); } } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 5eb208f1c7f..36781a62572 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action } from 'vs/base/common/actions'; +import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; import { SyncDescriptor0, createSyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IConstructorSignature2, createDecorator, BrandedService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindings, KeybindingsRegistry, IKeybindingRule } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -47,6 +47,7 @@ export interface IMenuItem { export interface ISubmenuItem { title: string | ILocalizedString; submenu: MenuId; + icon?: Icon; when?: ContextKeyExpression; group?: 'navigation' | string; order?: number; @@ -295,12 +296,35 @@ export class ExecuteCommandAction extends Action { } } -export class SubmenuItemAction extends Action { +export class SubmenuItemAction extends SubmenuAction { - readonly item: ISubmenuItem; - constructor(item: ISubmenuItem) { - typeof item.title === 'string' ? super('', item.title, 'submenu') : super('', item.title.value, 'submenu'); - this.item = item; + constructor( + readonly item: ISubmenuItem, + menuService: IMenuService, + contextKeyService: IContextKeyService, + options?: IMenuActionOptions + ) { + super(`submenuitem.${item.submenu.id}`, typeof item.title === 'string' ? item.title : item.title.value, () => { + const result: IAction[] = []; + const menu = menuService.createMenu(item.submenu, contextKeyService); + const groups = menu.getActions(options); + menu.dispose(); + + for (let group of groups) { + const [, actions] = group; + + if (actions.length > 0) { + result.push(...actions); + result.push(new Separator()); + } + } + + if (result.length) { + result.pop(); // remove last separator + } + + return result; + }, 'submenu'); } } diff --git a/src/vs/platform/actions/common/menuService.ts b/src/vs/platform/actions/common/menuService.ts index 107ea38d7fa..62c1dc8350e 100644 --- a/src/vs/platform/actions/common/menuService.ts +++ b/src/vs/platform/actions/common/menuService.ts @@ -20,7 +20,7 @@ export class MenuService implements IMenuService { } createMenu(id: MenuId, contextKeyService: IContextKeyService): IMenu { - return new Menu(id, this._commandService, contextKeyService); + return new Menu(id, this._commandService, contextKeyService, this); } } @@ -38,7 +38,8 @@ class Menu implements IMenu { constructor( private readonly _id: MenuId, @ICommandService private readonly _commandService: ICommandService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IMenuService private readonly _menuService: IMenuService ) { this._build(); @@ -114,7 +115,7 @@ class Menu implements IMenu { if (this._contextKeyService.contextMatchesRules(item.when)) { const action = isIMenuItem(item) ? new MenuItemAction(item.command, item.alt, options, this._contextKeyService, this._commandService) - : new SubmenuItemAction(item); + : new SubmenuItemAction(item, this._menuService, this._contextKeyService, options); activeActions.push(action); } diff --git a/src/vs/platform/browser/checkbox.ts b/src/vs/platform/browser/checkbox.ts index 7dd7a5fd8ef..c479126eeba 100644 --- a/src/vs/platform/browser/checkbox.ts +++ b/src/vs/platform/browser/checkbox.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionbar'; import { CheckboxActionViewItem } from 'vs/base/browser/ui/checkbox/checkbox'; import { IAction } from 'vs/base/common/actions'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachCheckboxStyler } from 'vs/platform/theme/common/styler'; +import { IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; export class ThemableCheckboxActionViewItem extends CheckboxActionViewItem { diff --git a/src/vs/platform/configuration/common/configuration.ts b/src/vs/platform/configuration/common/configuration.ts index b94c564686e..e09910f5458 100644 --- a/src/vs/platform/configuration/common/configuration.ts +++ b/src/vs/platform/configuration/common/configuration.ts @@ -10,7 +10,7 @@ import { Event } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IConfigurationRegistry, Extensions, OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationRegistry, Extensions, OVERRIDE_PROPERTY_PATTERN, overrideIdentifierFromKey } from 'vs/platform/configuration/common/configurationRegistry'; import { IStringDictionary } from 'vs/base/common/collections'; export const IConfigurationService = createDecorator('configurationService'); @@ -354,10 +354,6 @@ export function getDefaultValues(): any { return valueTreeRoot; } -export function overrideIdentifierFromKey(key: string): string { - return key.substring(1, key.length - 1); -} - export function keyFromOverrideIdentifier(overrideIdentifier: string): string { return `[${overrideIdentifier}]`; } diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index 1260ca7ac41..5af10fc3155 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -9,8 +9,8 @@ import * as arrays from 'vs/base/common/arrays'; import * as types from 'vs/base/common/types'; import * as objects from 'vs/base/common/objects'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { OVERRIDE_PROPERTY_PATTERN, ConfigurationScope, IConfigurationRegistry, Extensions, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; -import { IOverrides, overrideIdentifierFromKey, addToValueTree, toValuesTree, IConfigurationModel, getConfigurationValue, IConfigurationOverrides, IConfigurationData, getDefaultValues, getConfigurationKeys, removeFromValueTree, toOverrides, IConfigurationValue, ConfigurationTarget, compare, IConfigurationChangeEvent, IConfigurationChange } from 'vs/platform/configuration/common/configuration'; +import { OVERRIDE_PROPERTY_PATTERN, ConfigurationScope, IConfigurationRegistry, Extensions, IConfigurationPropertySchema, overrideIdentifierFromKey } from 'vs/platform/configuration/common/configurationRegistry'; +import { IOverrides, addToValueTree, toValuesTree, IConfigurationModel, getConfigurationValue, IConfigurationOverrides, IConfigurationData, getDefaultValues, getConfigurationKeys, removeFromValueTree, toOverrides, IConfigurationValue, ConfigurationTarget, compare, IConfigurationChangeEvent, IConfigurationChange } from 'vs/platform/configuration/common/configuration'; import { Workspace } from 'vs/platform/workspace/common/workspace'; import { Registry } from 'vs/platform/registry/common/platform'; import { Disposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index cfc588ae094..66096d08813 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -9,7 +9,7 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { Registry } from 'vs/platform/registry/common/platform'; import * as types from 'vs/base/common/types'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IStringDictionary } from 'vs/base/common/collections'; export const Extensions = { Configuration: 'base.contributions.configuration' @@ -35,12 +35,12 @@ export interface IConfigurationRegistry { /** * Register multiple default configurations to the registry. */ - registerDefaultConfigurations(defaultConfigurations: IDefaultConfigurationExtension[]): void; + registerDefaultConfigurations(defaultConfigurations: IStringDictionary[]): void; /** * Deregister multiple default configurations from the registry. */ - deregisterDefaultConfigurations(defaultConfigurations: IDefaultConfigurationExtension[]): void; + deregisterDefaultConfigurations(defaultConfigurations: IStringDictionary[]): void; /** * Signal that the schema of a configuration setting has changes. It is currently only supported to change enumeration values. @@ -131,12 +131,6 @@ export interface IConfigurationNode { extensionInfo?: IConfigurationExtensionInfo; } -export interface IDefaultConfigurationExtension { - id: ExtensionIdentifier; - name: string; - defaults: { [key: string]: {} }; -} - type SettingProperties = { [key: string]: any }; export const allSettings: { properties: SettingProperties, patternProperties: SettingProperties } = { properties: {}, patternProperties: {} }; @@ -152,7 +146,8 @@ const contributionRegistry = Registry.as(JSONExtensio class ConfigurationRegistry implements IConfigurationRegistry { - private readonly defaultOverridesConfigurationNode: IConfigurationNode; + private readonly defaultValues: IStringDictionary; + private readonly defaultLanguageConfigurationOverridesNode: IConfigurationNode; private readonly configurationContributors: IConfigurationNode[]; private readonly configurationProperties: { [qualifiedKey: string]: IJSONSchema }; private readonly excludedConfigurationProperties: { [qualifiedKey: string]: IJSONSchema }; @@ -166,12 +161,13 @@ class ConfigurationRegistry implements IConfigurationRegistry { readonly onDidUpdateConfiguration: Event = this._onDidUpdateConfiguration.event; constructor() { - this.defaultOverridesConfigurationNode = { + this.defaultValues = {}; + this.defaultLanguageConfigurationOverridesNode = { id: 'defaultOverrides', - title: nls.localize('defaultConfigurations.title', "Default Configuration Overrides"), + title: nls.localize('defaultLanguageConfigurationOverrides.title', "Default Language Configuration Overrides"), properties: {} }; - this.configurationContributors = [this.defaultOverridesConfigurationNode]; + this.configurationContributors = [this.defaultLanguageConfigurationOverridesNode]; this.resourceLanguageSettingsSchema = { properties: {}, patternProperties: {}, additionalProperties: false, errorMessage: 'Unknown editor configuration setting', allowTrailingCommas: true, allowComments: true }; this.configurationProperties = {}; this.excludedConfigurationProperties = {}; @@ -202,29 +198,8 @@ class ConfigurationRegistry implements IConfigurationRegistry { if (configuration.properties) { for (const key in configuration.properties) { properties.push(key); - delete this.configurationProperties[key]; - - // Delete from schema - delete allSettings.properties[key]; - switch (configuration.properties[key].scope) { - case ConfigurationScope.APPLICATION: - delete applicationSettings.properties[key]; - break; - case ConfigurationScope.MACHINE: - delete machineSettings.properties[key]; - break; - case ConfigurationScope.MACHINE_OVERRIDABLE: - delete machineOverridableSettings.properties[key]; - break; - case ConfigurationScope.WINDOW: - delete windowSettings.properties[key]; - break; - case ConfigurationScope.RESOURCE: - case ConfigurationScope.LANGUAGE_OVERRIDABLE: - delete resourceSettings.properties[key]; - break; - } + this.removeFromSchema(key, configuration.properties[key]); } } if (configuration.allOf) { @@ -244,41 +219,60 @@ class ConfigurationRegistry implements IConfigurationRegistry { this._onDidUpdateConfiguration.fire(properties); } - public registerDefaultConfigurations(defaultConfigurations: IDefaultConfigurationExtension[]): void { + public registerDefaultConfigurations(defaultConfigurations: IStringDictionary[]): void { const properties: string[] = []; + const overrideIdentifiers: string[] = []; for (const defaultConfiguration of defaultConfigurations) { - for (const key in defaultConfiguration.defaults) { - const defaultValue = defaultConfiguration.defaults[key]; - if (OVERRIDE_PROPERTY_PATTERN.test(key) && typeof defaultValue === 'object') { - const propertySchema: IConfigurationPropertySchema = { + for (const key in defaultConfiguration) { + properties.push(key); + this.defaultValues[key] = defaultConfiguration[key]; + + if (OVERRIDE_PROPERTY_PATTERN.test(key)) { + const property: IConfigurationPropertySchema = { type: 'object', - default: defaultValue, - description: nls.localize('overrideSettings.description', "Configure editor settings to be overridden for {0} language.", key), + default: this.defaultValues[key], + description: nls.localize('defaultLanguageConfiguration.description', "Configure settings to be overridden for {0} language.", key), $ref: resourceLanguageSettingsSchemaId }; - allSettings.properties[key] = propertySchema; - this.defaultOverridesConfigurationNode.properties![key] = propertySchema; - this.configurationProperties[key] = propertySchema; - properties.push(key); + overrideIdentifiers.push(overrideIdentifierFromKey(key)); + this.configurationProperties[key] = property; + this.defaultLanguageConfigurationOverridesNode.properties![key] = property; + } else { + const property = this.configurationProperties[key]; + if (property) { + this.updatePropertyDefaultValue(key, property); + this.updateSchema(key, property); + } } } } + this.registerOverrideIdentifiers(overrideIdentifiers); this._onDidSchemaChange.fire(); this._onDidUpdateConfiguration.fire(properties); } - public deregisterDefaultConfigurations(defaultConfigurations: IDefaultConfigurationExtension[]): void { + public deregisterDefaultConfigurations(defaultConfigurations: IStringDictionary[]): void { const properties: string[] = []; for (const defaultConfiguration of defaultConfigurations) { - for (const key in defaultConfiguration.defaults) { + for (const key in defaultConfiguration) { properties.push(key); - delete allSettings.properties[key]; - delete this.defaultOverridesConfigurationNode.properties![key]; - delete this.configurationProperties[key]; + delete this.defaultValues[key]; + if (OVERRIDE_PROPERTY_PATTERN.test(key)) { + delete this.configurationProperties[key]; + delete this.defaultLanguageConfigurationOverridesNode.properties![key]; + } else { + const property = this.configurationProperties[key]; + if (property) { + this.updatePropertyDefaultValue(key, property); + this.updateSchema(key, property); + } + } } } + + this.updateOverridePropertyPatternKey(); this._onDidSchemaChange.fire(); this._onDidUpdateConfiguration.fire(properties); } @@ -291,7 +285,6 @@ class ConfigurationRegistry implements IConfigurationRegistry { for (const overrideIdentifier of overrideIdentifiers) { this.overrideIdentifiers.add(overrideIdentifier); } - this.updateOverridePropertyPatternKey(); } @@ -305,12 +298,13 @@ class ConfigurationRegistry implements IConfigurationRegistry { delete properties[key]; continue; } - // fill in default values - let property = properties[key]; - let defaultValue = property.default; - if (types.isUndefined(defaultValue)) { - property.default = getDefaultValue(property.type); - } + + const property = properties[key]; + + // update default value + this.updatePropertyDefaultValue(key, property); + + // update scope if (OVERRIDE_PROPERTY_PATTERN.test(key)) { property.scope = undefined; // No scope for overridable properties `[${identifier}]` } else { @@ -361,28 +355,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { let properties = configuration.properties; if (properties) { for (const key in properties) { - allSettings.properties[key] = properties[key]; - switch (properties[key].scope) { - case ConfigurationScope.APPLICATION: - applicationSettings.properties[key] = properties[key]; - break; - case ConfigurationScope.MACHINE: - machineSettings.properties[key] = properties[key]; - break; - case ConfigurationScope.MACHINE_OVERRIDABLE: - machineOverridableSettings.properties[key] = properties[key]; - break; - case ConfigurationScope.WINDOW: - windowSettings.properties[key] = properties[key]; - break; - case ConfigurationScope.RESOURCE: - resourceSettings.properties[key] = properties[key]; - break; - case ConfigurationScope.LANGUAGE_OVERRIDABLE: - resourceSettings.properties[key] = properties[key]; - this.resourceLanguageSettingsSchema.properties![key] = properties[key]; - break; - } + this.updateSchema(key, properties[key]); } } let subNodes = configuration.allOf; @@ -393,6 +366,53 @@ class ConfigurationRegistry implements IConfigurationRegistry { register(configuration); } + private updateSchema(key: string, property: IConfigurationPropertySchema): void { + allSettings.properties[key] = property; + switch (property.scope) { + case ConfigurationScope.APPLICATION: + applicationSettings.properties[key] = property; + break; + case ConfigurationScope.MACHINE: + machineSettings.properties[key] = property; + break; + case ConfigurationScope.MACHINE_OVERRIDABLE: + machineOverridableSettings.properties[key] = property; + break; + case ConfigurationScope.WINDOW: + windowSettings.properties[key] = property; + break; + case ConfigurationScope.RESOURCE: + resourceSettings.properties[key] = property; + break; + case ConfigurationScope.LANGUAGE_OVERRIDABLE: + resourceSettings.properties[key] = property; + this.resourceLanguageSettingsSchema.properties![key] = property; + break; + } + } + + private removeFromSchema(key: string, property: IConfigurationPropertySchema): void { + delete allSettings.properties[key]; + switch (property.scope) { + case ConfigurationScope.APPLICATION: + delete applicationSettings.properties[key]; + break; + case ConfigurationScope.MACHINE: + delete machineSettings.properties[key]; + break; + case ConfigurationScope.MACHINE_OVERRIDABLE: + delete machineOverridableSettings.properties[key]; + break; + case ConfigurationScope.WINDOW: + delete windowSettings.properties[key]; + break; + case ConfigurationScope.RESOURCE: + case ConfigurationScope.LANGUAGE_OVERRIDABLE: + delete resourceSettings.properties[key]; + break; + } + } + private updateOverridePropertyPatternKey(): void { for (const overrideIdentifier of this.overrideIdentifiers.values()) { const overrideIdentifierProperty = `[${overrideIdentifier}]`; @@ -401,8 +421,8 @@ class ConfigurationRegistry implements IConfigurationRegistry { description: nls.localize('overrideSettings.defaultDescription', "Configure editor settings to be overridden for a language."), errorMessage: nls.localize('overrideSettings.errorMessage', "This setting does not support per-language configuration."), $ref: resourceLanguageSettingsSchemaId, - default: this.defaultOverridesConfigurationNode.properties![overrideIdentifierProperty]?.default }; + this.updatePropertyDefaultValue(overrideIdentifierProperty, resourceLanguagePropertiesSchema); allSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; applicationSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; machineSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; @@ -412,11 +432,26 @@ class ConfigurationRegistry implements IConfigurationRegistry { } this._onDidSchemaChange.fire(); } + + private updatePropertyDefaultValue(key: string, property: IConfigurationPropertySchema): void { + let defaultValue = this.defaultValues[key]; + if (types.isUndefined(defaultValue)) { + defaultValue = property.default; + } + if (types.isUndefined(defaultValue)) { + defaultValue = getDefaultValue(property.type); + } + property.default = defaultValue; + } } const OVERRIDE_PROPERTY = '\\[.*\\]$'; export const OVERRIDE_PROPERTY_PATTERN = new RegExp(OVERRIDE_PROPERTY); +export function overrideIdentifierFromKey(key: string): string { + return key.substring(1, key.length - 1); +} + export function getDefaultValue(type: string | string[] | undefined): any { const t = Array.isArray(type) ? (type)[0] : type; switch (t) { diff --git a/src/vs/platform/contextview/browser/contextMenuHandler.css b/src/vs/platform/contextview/browser/contextMenuHandler.css index ef8a5236187..51a9e400923 100644 --- a/src/vs/platform/contextview/browser/contextMenuHandler.css +++ b/src/vs/platform/contextview/browser/contextMenuHandler.css @@ -7,11 +7,3 @@ min-width: 130px; } -.context-view-block { - position: fixed; - cursor: initial; - left:0; - top:0; - width: 100%; - height: 100%; -} diff --git a/src/vs/platform/contextview/browser/contextMenuHandler.ts b/src/vs/platform/contextview/browser/contextMenuHandler.ts index 3daf186140d..1804ce3328e 100644 --- a/src/vs/platform/contextview/browser/contextMenuHandler.ts +++ b/src/vs/platform/contextview/browser/contextMenuHandler.ts @@ -50,7 +50,7 @@ export class ContextMenuHandler { let menu: Menu | undefined; - const anchor = delegate.getAnchor(); + let shadowRootElement = isHTMLElement(delegate.domForShadowRoot) ? delegate.domForShadowRoot : undefined; this.contextViewService.showContextView({ getAnchor: () => delegate.getAnchor(), canRelayout: false, @@ -66,6 +66,13 @@ export class ContextMenuHandler { // Render invisible div to block mouse interaction in the rest of the UI if (this.options.blockMouse) { this.block = container.appendChild($('.context-view-block')); + this.block.style.position = 'fixed'; + this.block.style.cursor = 'initial'; + this.block.style.left = '0'; + this.block.style.top = '0'; + this.block.style.width = '100%'; + this.block.style.height = '100%'; + this.block.style.zIndex = '-1'; domEvent(this.block, EventType.MOUSE_DOWN)((e: MouseEvent) => e.stopPropagation()); } @@ -133,7 +140,7 @@ export class ContextMenuHandler { this.focusToReturn.focus(); } } - }, !!delegate.anchorAsContainer && isHTMLElement(anchor) ? anchor : undefined); + }, shadowRootElement, !!shadowRootElement); } private onActionRun(e: IRunEvent): void { diff --git a/src/vs/platform/contextview/browser/contextView.ts b/src/vs/platform/contextview/browser/contextView.ts index fde55c58e76..c6511397a9f 100644 --- a/src/vs/platform/contextview/browser/contextView.ts +++ b/src/vs/platform/contextview/browser/contextView.ts @@ -15,8 +15,9 @@ export interface IContextViewService extends IContextViewProvider { readonly _serviceBrand: undefined; - showContextView(delegate: IContextViewDelegate, container?: HTMLElement): IDisposable; + showContextView(delegate: IContextViewDelegate, container?: HTMLElement, shadowRoot?: boolean): IDisposable; hideContextView(data?: any): void; + getContextViewElement(): HTMLElement; layout(): void; anchorAlignment?: AnchorAlignment; } diff --git a/src/vs/platform/contextview/browser/contextViewService.ts b/src/vs/platform/contextview/browser/contextViewService.ts index 685b7e21cff..cb7578f0500 100644 --- a/src/vs/platform/contextview/browser/contextViewService.ts +++ b/src/vs/platform/contextview/browser/contextViewService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IContextViewService, IContextViewDelegate } from './contextView'; -import { ContextView } from 'vs/base/browser/ui/contextview/contextview'; +import { ContextView, ContextViewDOMPosition } from 'vs/base/browser/ui/contextview/contextview'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; @@ -21,7 +21,7 @@ export class ContextViewService extends Disposable implements IContextViewServic super(); this.container = layoutService.container; - this.contextView = this._register(new ContextView(this.container, false)); + this.contextView = this._register(new ContextView(this.container, ContextViewDOMPosition.ABSOLUTE)); this.layout(); this._register(layoutService.onLayout(() => this.layout())); @@ -29,20 +29,20 @@ export class ContextViewService extends Disposable implements IContextViewServic // ContextView - setContainer(container: HTMLElement, useFixedPosition?: boolean): void { - this.contextView.setContainer(container, !!useFixedPosition); + setContainer(container: HTMLElement, domPosition?: ContextViewDOMPosition): void { + this.contextView.setContainer(container, domPosition || ContextViewDOMPosition.ABSOLUTE); } - showContextView(delegate: IContextViewDelegate, container?: HTMLElement): IDisposable { + showContextView(delegate: IContextViewDelegate, container?: HTMLElement, shadowRoot?: boolean): IDisposable { if (container) { if (container !== this.container) { this.container = container; - this.setContainer(container, true); + this.setContainer(container, shadowRoot ? ContextViewDOMPosition.FIXED_SHADOW : ContextViewDOMPosition.FIXED); } } else { if (this.container !== this.layoutService.container) { this.container = this.layoutService.container; - this.setContainer(this.container, false); + this.setContainer(this.container, ContextViewDOMPosition.ABSOLUTE); } } @@ -58,6 +58,10 @@ export class ContextViewService extends Disposable implements IContextViewServic return disposable; } + getContextViewElement(): HTMLElement { + return this.contextView.getViewElement(); + } + layout(): void { this.contextView.layout(); } diff --git a/src/vs/platform/electron/common/electron.ts b/src/vs/platform/electron/common/electron.ts index d34b1c42a73..1d65ceea6c6 100644 --- a/src/vs/platform/electron/common/electron.ts +++ b/src/vs/platform/electron/common/electron.ts @@ -64,6 +64,10 @@ export interface ICommonElectronService { updateTouchBar(items: ISerializableCommandAction[][]): Promise; moveItemToTrash(fullPath: string, deleteOnFail?: boolean): Promise; isAdmin(): Promise; + getTotalMem(): Promise; + + // Process + killProcess(pid: number, code: string): Promise; // clipboard readClipboardText(type?: 'selection' | 'clipboard'): Promise; diff --git a/src/vs/platform/electron/electron-main/electronMainService.ts b/src/vs/platform/electron/electron-main/electronMainService.ts index 12da10f915c..0976846791f 100644 --- a/src/vs/platform/electron/electron-main/electronMainService.ts +++ b/src/vs/platform/electron/electron-main/electronMainService.ts @@ -23,6 +23,7 @@ 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'; export interface IElectronMainService extends AddFirstParameterToFunctions /* only methods, not events */, number | undefined /* window ID */> { } @@ -313,6 +314,19 @@ export class ElectronMainService implements IElectronMainService { return isAdmin; } + async getTotalMem(): Promise { + return totalmem(); + } + + //#endregion + + + //#region Process + + async killProcess(windowId: number | undefined, pid: number, code: string): Promise { + process.kill(pid, code); + } + //#endregion diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index d3179e46b2f..2379b626c81 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -70,12 +70,13 @@ export interface ParsedArgs { 'file-chmod'?: boolean; 'driver'?: string; 'driver-verbose'?: boolean; - remote?: string; + 'remote'?: string; 'disable-user-env-probe'?: boolean; 'force'?: boolean; 'do-not-sync'?: boolean; 'force-user-env'?: boolean; 'sync'?: 'on' | 'off'; + '__sandbox'?: boolean; // chromium command line args: https://electronjs.org/docs/all#supported-chrome-command-line-switches 'no-proxy-server'?: boolean; @@ -195,6 +196,7 @@ export const OPTIONS: OptionDescriptions> = { 'trace-options': { type: 'string' }, 'force-user-env': { type: 'boolean' }, 'open-devtools': { type: 'boolean' }, + '__sandbox': { type: 'boolean' }, // chromium flags 'no-proxy-server': { type: 'boolean' }, diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 571418e3df3..5c0dc4ad4ae 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -43,6 +43,8 @@ export interface INativeEnvironmentService extends IEnvironmentService { driverVerbose: boolean; disableUpdates: boolean; + + sandbox: boolean; } export class EnvironmentService implements INativeEnvironmentService { @@ -262,6 +264,8 @@ export class EnvironmentService implements INativeEnvironmentService { get disableTelemetry(): boolean { return !!this._args['disable-telemetry']; } + get sandbox(): boolean { return !!this._args['__sandbox']; } + constructor(private _args: ParsedArgs, private _execPath: string) { if (!process.env['VSCODE_LOGS']) { const key = toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, ''); diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 624ecc44a47..52949788147 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -162,7 +162,7 @@ class Query { withFilter(filterType: FilterType, ...values: string[]): Query { const criteria = [ ...this.state.criteria, - ...values.map(value => ({ filterType, value })) + ...values.length ? values.map(value => ({ filterType, value })) : [{ filterType }] ]; return new Query(assign({}, this.state, { criteria })); @@ -441,6 +441,12 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return ''; }); + // Use featured filter + text = text.replace(/\bfeatured(\s+|\b|$)/g, () => { + query = query.withFilter(FilterType.Featured); + return ''; + }); + text = text.trim(); if (text) { diff --git a/src/vs/platform/extensionManagement/node/extensionTipsService.ts b/src/vs/platform/extensionManagement/node/extensionTipsService.ts index e1ad6092e0b..5780216da37 100644 --- a/src/vs/platform/extensionManagement/node/extensionTipsService.ts +++ b/src/vs/platform/extensionManagement/node/extensionTipsService.ts @@ -89,6 +89,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService { } } else { exePaths.push(join('/usr/local/bin', exeName)); + exePaths.push(join('/usr/bin', exeName)); exePaths.push(join(this.environmentService.userHome.fsPath, exeName)); } diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 17673328b27..6024a71fb10 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -142,8 +142,25 @@ export interface IExtensionIdentifier { uuid?: string; } -export const EXTENSION_CATEGORIES = ['Programming Languages', 'Snippets', 'Linters', 'Themes', 'Debuggers', 'Other', 'Keymaps', 'Formatters', 'Extension Packs', - 'SCM Providers', 'Azure', 'Language Packs', 'Data Science', 'Machine Learning', 'Visualization', 'Testing', 'Notebooks']; +export const EXTENSION_CATEGORIES = [ + 'Azure', + 'Data Science', + 'Debuggers', + 'Extension Packs', + 'Formatters', + 'Keymaps', + 'Language Packs', + 'Linters', + 'Machine Learning', + 'Notebooks', + 'Programming Languages', + 'SCM Providers', + 'Snippets', + 'Themes', + 'Testing', + 'Visualization', + 'Other', +]; export interface IExtensionManifest { readonly name: string; @@ -255,12 +272,22 @@ export interface IScannedExtension { readonly identifier: IExtensionIdentifier; readonly location: URI; readonly type: ExtensionType; - readonly packageJSON: IExtensionManifest + readonly packageJSON: IExtensionManifest; + readonly packageNLS?: any; readonly packageNLSUrl?: URI; readonly readmeUrl?: URI; readonly changelogUrl?: URI; } +export interface ITranslatedScannedExtension { + readonly identifier: IExtensionIdentifier; + readonly location: URI; + readonly type: ExtensionType; + readonly packageJSON: IExtensionManifest; + readonly readmeUrl?: URI; + readonly changelogUrl?: URI; +} + export const IBuiltinExtensionsScannerService = createDecorator('IBuiltinExtensionsScannerService'); export interface IBuiltinExtensionsScannerService { readonly _serviceBrand: undefined; diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index b8b4bdfd1ee..39b57391424 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -29,7 +29,7 @@ export interface IFileService { readonly onDidChangeFileSystemProviderRegistrations: Event; /** - * An even that is fired when a registered file system provider changes it's capabilities. + * An event that is fired when a registered file system provider changes it's capabilities. */ readonly onDidChangeFileSystemProviderCapabilities: Event; diff --git a/src/vs/platform/issue/common/issue.ts b/src/vs/platform/issue/common/issue.ts index d95587fce0a..d82ba0948de 100644 --- a/src/vs/platform/issue/common/issue.ts +++ b/src/vs/platform/issue/common/issue.ts @@ -85,6 +85,8 @@ export interface ProcessExplorerStyles extends WindowStyles { export interface ProcessExplorerData extends WindowData { pid: number; styles: ProcessExplorerStyles; + platform: string; + applicationName: string; } export interface ICommonIssueService { diff --git a/src/vs/platform/issue/electron-main/issueMainService.ts b/src/vs/platform/issue/electron-main/issueMainService.ts index 532a3bd4e59..48fae538c08 100644 --- a/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/src/vs/platform/issue/electron-main/issueMainService.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; +import * as os from 'os'; +import product from 'vs/platform/product/common/product'; import * as objects from 'vs/base/common/objects'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; import { ICommonIssueService, IssueReporterData, IssueReporterFeatures, ProcessExplorerData } from 'vs/platform/issue/common/issue'; @@ -196,11 +198,22 @@ export class IssueMainService implements ICommonIssueService { backgroundColor: data.styles.backgroundColor || DEFAULT_BACKGROUND_COLOR, webPreferences: { preload: URI.parse(require.toUrl('vs/base/parts/sandbox/electron-browser/preload.js')).fsPath, - nodeIntegration: true, enableWebSQL: false, enableRemoteModule: false, nativeWindowOpen: true, - zoomFactor: zoomLevelToZoomFactor(data.zoomLevel) + zoomFactor: zoomLevelToZoomFactor(data.zoomLevel), + ...this.environmentService.sandbox ? + + // Sandbox + { + sandbox: true, + contextIsolation: true + } : + + // No Sandbox + { + nodeIntegration: true + } } }); @@ -250,11 +263,22 @@ export class IssueMainService implements ICommonIssueService { title: localize('processExplorer', "Process Explorer"), webPreferences: { preload: URI.parse(require.toUrl('vs/base/parts/sandbox/electron-browser/preload.js')).fsPath, - nodeIntegration: true, enableWebSQL: false, enableRemoteModule: false, nativeWindowOpen: true, - zoomFactor: zoomLevelToZoomFactor(data.zoomLevel) + zoomFactor: zoomLevelToZoomFactor(data.zoomLevel), + ...this.environmentService.sandbox ? + + // Sandbox + { + sandbox: true, + contextIsolation: true + } : + + // No Sandbox + { + nodeIntegration: true + } } }); @@ -270,7 +294,7 @@ export class IssueMainService implements ICommonIssueService { }; this._processExplorerWindow.loadURL( - toLauchUrl('vs/code/electron-browser/processExplorer/processExplorer.html', windowConfiguration)); + toLauchUrl('vs/code/electron-sandbox/processExplorer/processExplorer.html', windowConfiguration)); this._processExplorerWindow.on('close', () => this._processExplorerWindow = null); @@ -395,10 +419,23 @@ export class IssueMainService implements ICommonIssueService { machineId: this.machineId, userEnv: this.userEnv, data, - features + features, + disableExtensions: this.environmentService.disableExtensions, + os: { + type: os.type(), + arch: os.arch(), + release: os.release(), + }, + product: { + nameShort: product.nameShort, + version: product.version, + commit: product.commit, + date: product.date, + reportIssueUrl: product.reportIssueUrl + } }; - return toLauchUrl('vs/code/electron-browser/issue/issueReporter.html', windowConfiguration); + return toLauchUrl('vs/code/electron-sandbox/issue/issueReporter.html', windowConfiguration); } } diff --git a/src/vs/platform/label/common/label.ts b/src/vs/platform/label/common/label.ts index 15a5511ddc9..d1739d0a6d6 100644 --- a/src/vs/platform/label/common/label.ts +++ b/src/vs/platform/label/common/label.ts @@ -49,4 +49,5 @@ export interface ResourceLabelFormatting { normalizeDriveLetter?: boolean; workspaceSuffix?: string; authorityPrefix?: string; + stripPathStartingSeparator?: boolean; } diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 40a2cf28940..233f1690c98 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -156,8 +156,6 @@ export class LaunchMainService implements ILaunchMainService { else { const lastActive = this.windowsMainService.getLastActiveWindow(); if (lastActive) { - // Force focus the app before requesting window focus - app.focus({ steal: true }); lastActive.focus(); usedWindows = [lastActive]; diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index ea3a652aa7e..ead6b3a943a 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -531,11 +531,6 @@ abstract class ResourceNavigator extends Disposable { browserEvent }); } - - // hack for References Widget: pressing Enter on already selected tree element - open(browserEvent?: UIEvent): void { - this._open((browserEvent as any)?.preserveFocus || false, true, false, browserEvent); - } } export class ListResourceNavigator extends ResourceNavigator { @@ -608,10 +603,6 @@ export class WorkbenchObjectTree, TFilterData = void> this.internals = new WorkbenchTreeInternals(this, options, getAutomaticKeyboardNavigation, options.overrideStyles, contextKeyService, listService, themeService, configurationService, accessibilityService); this.disposables.add(this.internals); } - - open(browserEvent?: UIEvent): void { - this.internals.open(browserEvent); - } } export interface IWorkbenchCompressibleObjectTreeOptionsUpdate extends ICompressibleObjectTreeOptionsUpdate { @@ -656,10 +647,6 @@ export class WorkbenchCompressibleObjectTree, TFilter this.internals.updateStyleOverrides(options.overrideStyles); } } - - open(browserEvent?: UIEvent): void { - this.internals.open(browserEvent); - } } export interface IWorkbenchDataTreeOptionsUpdate extends IAbstractTreeOptionsUpdate { @@ -705,10 +692,6 @@ export class WorkbenchDataTree extends DataTree extends Async this.internals.updateStyleOverrides(options.overrideStyles); } } - - open(browserEvent?: UIEvent): void { - this.internals.open(browserEvent); - } } export interface IWorkbenchCompressibleAsyncDataTreeOptions extends ICompressibleAsyncDataTreeOptions, IResourceNavigatorOptions { @@ -793,10 +772,6 @@ export class WorkbenchCompressibleAsyncDataTree e this.internals = new WorkbenchTreeInternals(this, options, getAutomaticKeyboardNavigation, options.overrideStyles, contextKeyService, listService, themeService, configurationService, accessibilityService); this.disposables.add(this.internals); } - - open(browserEvent?: UIEvent): void { - this.internals.open(browserEvent); - } } function workbenchTreeDataPreamble | IAsyncDataTreeOptions>( @@ -975,10 +950,6 @@ class WorkbenchTreeInternals { this.styler = overrideStyles ? attachListStyler(this.tree, this.themeService, overrideStyles) : Disposable.None; } - open(browserEvent?: UIEvent): void { - this.navigator.open(browserEvent); - } - dispose(): void { this.disposables = dispose(this.disposables); dispose(this.styler); diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 67083e24df7..3370a608b4b 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -23,7 +23,11 @@ if (isWeb) { version: '1.48.0-dev', nameLong: 'Visual Studio Code Web Dev', nameShort: 'VSCode Web Dev', - urlProtocol: 'code-oss' + urlProtocol: 'code-oss', + extensionAllowedProposedApi: [ + 'ms-vscode.references-view', + 'ms-vscode.github-browser' + ], }); } } diff --git a/src/vs/platform/product/common/productService.ts b/src/vs/platform/product/common/productService.ts index 188786627ce..040c869d94c 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts @@ -22,7 +22,12 @@ export interface IBuiltInExtension { readonly metadata: any; } -export type ConfigurationSyncStore = { url: string, authenticationProviders: IStringDictionary<{ scopes: string[] }> }; +export type ConfigurationSyncStore = { + url: string, + insidersUrl?: string, + stableUrl?: string, + authenticationProviders: IStringDictionary<{ scopes: string[] }> +}; export interface IProductConfiguration { readonly version: string; @@ -49,6 +54,13 @@ export interface IProductConfiguration { readonly settingsSearchBuildId?: number; readonly settingsSearchUrl?: string; + readonly tasConfig?: { + endpoint: string; + telemetryEventName: string; + featuresTelemetryPropertyName: string; + assignmentContextTelemetryPropertyName: string; + }; + readonly experimentsUrl?: string; readonly extensionsGallery?: { diff --git a/src/vs/platform/remote/browser/browserSocketFactory.ts b/src/vs/platform/remote/browser/browserSocketFactory.ts index d0f6e6b18a6..3715cbb8e6e 100644 --- a/src/vs/platform/remote/browser/browserSocketFactory.ts +++ b/src/vs/platform/remote/browser/browserSocketFactory.ts @@ -194,6 +194,9 @@ class BrowserSocket implements ISocket { this.socket.close(); } + public drain(): Promise { + return Promise.resolve(); + } } diff --git a/src/vs/platform/remote/common/remoteAgentConnection.ts b/src/vs/platform/remote/common/remoteAgentConnection.ts index eab85914921..2185bb5228c 100644 --- a/src/vs/platform/remote/common/remoteAgentConnection.ts +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts @@ -14,6 +14,7 @@ import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors import { ISignService } from 'vs/platform/sign/common/sign'; 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'; export const enum ConnectionType { Management = 1, @@ -246,6 +247,7 @@ export interface IConnectionOptions { addressProvider: IAddressProvider; signService: ISignService; logService: ILogService; + ipcLogger: IIPCLogger | null; } async function resolveConnectionOptions(options: IConnectionOptions, reconnectionToken: string, reconnectionProtocol: PersistentProtocol | null): Promise { @@ -493,7 +495,7 @@ export class ManagementPersistentConnection extends PersistentConnection { this.client = this._register(new Client(protocol, { remoteAuthority: remoteAuthority, clientId: clientId - })); + }, options.ipcLogger)); } protected async _reconnect(options: ISimpleConnectionOptions): Promise { diff --git a/src/vs/platform/remote/common/remoteAgentEnvironment.ts b/src/vs/platform/remote/common/remoteAgentEnvironment.ts index 81028f0af4b..3052141ff01 100644 --- a/src/vs/platform/remote/common/remoteAgentEnvironment.ts +++ b/src/vs/platform/remote/common/remoteAgentEnvironment.ts @@ -5,7 +5,6 @@ import { URI } from 'vs/base/common/uri'; import { OperatingSystem } from 'vs/base/common/platform'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; export interface IRemoteAgentEnvironment { pid: number; @@ -18,7 +17,6 @@ export interface IRemoteAgentEnvironment { globalStorageHome: URI; workspaceStorageHome: URI; userHome: URI; - extensions: IExtensionDescription[]; os: OperatingSystem; } diff --git a/src/vs/platform/remote/node/tunnelService.ts b/src/vs/platform/remote/node/tunnelService.ts index b6b111df65c..983401be417 100644 --- a/src/vs/platform/remote/node/tunnelService.ts +++ b/src/vs/platform/remote/node/tunnelService.ts @@ -151,7 +151,8 @@ export class TunnelService extends AbstractTunnelService { socketFactory: nodeSocketFactory, addressProvider, signService: this.signService, - logService: this.logService + logService: this.logService, + ipcLogger: null }; const tunnel = createRemoteTunnel(options, remoteHost, remotePort, localPort); diff --git a/src/vs/platform/sign/node/signService.ts b/src/vs/platform/sign/node/signService.ts index 21c8156e7c3..7109d3fa70d 100644 --- a/src/vs/platform/sign/node/signService.ts +++ b/src/vs/platform/sign/node/signService.ts @@ -6,6 +6,7 @@ import { ISignService } from 'vs/platform/sign/common/sign'; declare module vsda { + // the signer is a native module that for historical reasons uses a lower case class name // eslint-disable-next-line @typescript-eslint/naming-convention export class signer { sign(arg: any): any; diff --git a/src/vs/platform/telemetry/common/telemetry.ts b/src/vs/platform/telemetry/common/telemetry.ts index 5b165a34e29..523f81e8d0b 100644 --- a/src/vs/platform/telemetry/common/telemetry.ts +++ b/src/vs/platform/telemetry/common/telemetry.ts @@ -46,6 +46,8 @@ export interface ITelemetryService { getTelemetryInfo(): Promise; + setExperimentProperty(name: string, value: string): void; + isOptedIn: boolean; } diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index 49c4d59d5d5..1e1c6fcb583 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -31,6 +31,7 @@ export class TelemetryService implements ITelemetryService { private _appender: ITelemetryAppender; private _commonProperties: Promise<{ [name: string]: any; }>; + private _experimentProperties: { [name: string]: string } = {}; private _piiPaths: string[]; private _userOptIn: boolean; private _enabled: boolean; @@ -79,6 +80,10 @@ export class TelemetryService implements ITelemetryService { } } + setExperimentProperty(name: string, value: string): void { + this._experimentProperties[name] = value; + } + setEnabled(value: boolean): void { this._enabled = value; } @@ -119,6 +124,9 @@ export class TelemetryService implements ITelemetryService { // (first) add common properties data = mixin(data, values); + // (next) add experiment properties + data = mixin(data, this._experimentProperties); + // (last) remove all PII from data data = cloneAndChange(data, value => { if (typeof value === 'string') { diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index ae0a2110009..392c3ad8ea8 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -28,6 +28,7 @@ export const NullTelemetryService = new class implements ITelemetryService { return this.publicLogError(eventName, data as ITelemetryData); } + setExperimentProperty() { } setEnabled() { } isOptedIn = true; getTelemetryInfo(): Promise { diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index afcc3102022..673f3fb3d93 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -390,7 +390,7 @@ export const menuSeparatorBackground = registerColor('menu.separatorBackground', export const snippetTabstopHighlightBackground = registerColor('editor.snippetTabstopHighlightBackground', { dark: new Color(new RGBA(124, 124, 124, 0.3)), light: new Color(new RGBA(10, 50, 100, 0.2)), hc: new Color(new RGBA(124, 124, 124, 0.3)) }, nls.localize('snippetTabstopHighlightBackground', "Highlight background color of a snippet tabstop.")); export const snippetTabstopHighlightBorder = registerColor('editor.snippetTabstopHighlightBorder', { dark: null, light: null, hc: null }, nls.localize('snippetTabstopHighlightBorder', "Highlight border color of a snippet tabstop.")); export const snippetFinalTabstopHighlightBackground = registerColor('editor.snippetFinalTabstopHighlightBackground', { dark: null, light: null, hc: null }, nls.localize('snippetFinalTabstopHighlightBackground', "Highlight background color of the final tabstop of a snippet.")); -export const snippetFinalTabstopHighlightBorder = registerColor('editor.snippetFinalTabstopHighlightBorder', { dark: '#525252', light: new Color(new RGBA(10, 50, 100, 0.5)), hc: '#525252' }, nls.localize('snippetFinalTabstopHighlightBorder', "Highlight border color of the final stabstop of a snippet.")); +export const snippetFinalTabstopHighlightBorder = registerColor('editor.snippetFinalTabstopHighlightBorder', { dark: '#525252', light: new Color(new RGBA(10, 50, 100, 0.5)), hc: '#525252' }, nls.localize('snippetFinalTabstopHighlightBorder', "Highlight border color of the final tabstop of a snippet.")); /** * Breadcrumb colors diff --git a/src/vs/platform/undoRedo/common/undoRedoService.ts b/src/vs/platform/undoRedo/common/undoRedoService.ts index 99b06d9ed73..a42d889cc8b 100644 --- a/src/vs/platform/undoRedo/common/undoRedoService.ts +++ b/src/vs/platform/undoRedo/common/undoRedoService.ts @@ -82,10 +82,19 @@ class RemovedResources { let messages: string[] = []; if (externalRemoval.length > 0) { - messages.push(nls.localize('externalRemoval', "The following files have been closed and modified on disk: {0}.", externalRemoval.join(', '))); + messages.push( + nls.localize( + { key: 'externalRemoval', comment: ['{0} is a list of filenames'] }, + "The following files have been closed and modified on disk: {0}.", externalRemoval.join(', ') + ) + ); } if (noParallelUniverses.length > 0) { - messages.push(nls.localize('noParallelUniverses', "The following files have been modified in an incompatible way: {0}.", noParallelUniverses.join(', '))); + messages.push( + nls.localize( + { key: 'noParallelUniverses', comment: ['{0} is a list of filenames'] }, + "The following files have been modified in an incompatible way: {0}.", noParallelUniverses.join(', ') + )); } return messages.join('\n'); } @@ -771,10 +780,26 @@ export class UndoRedoService implements IUndoRedoService { private _checkWorkspaceUndo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot, checkInvalidatedResources: boolean): WorkspaceVerificationError | null { if (element.removedResources) { - return this._tryToSplitAndUndo(strResource, element, element.removedResources, nls.localize('cannotWorkspaceUndo', "Could not undo '{0}' across all files. {1}", element.label, element.removedResources.createMessage())); + return this._tryToSplitAndUndo( + strResource, + element, + element.removedResources, + nls.localize( + { key: 'cannotWorkspaceUndo', comment: ['{0} is a label for an operation. {1} is another message.'] }, + "Could not undo '{0}' across all files. {1}", element.label, element.removedResources.createMessage() + ) + ); } if (checkInvalidatedResources && element.invalidatedResources) { - return this._tryToSplitAndUndo(strResource, element, element.invalidatedResources, nls.localize('cannotWorkspaceUndo', "Could not undo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage())); + return this._tryToSplitAndUndo( + strResource, + element, + element.invalidatedResources, + nls.localize( + { key: 'cannotWorkspaceUndo', comment: ['{0} is a label for an operation. {1} is another message.'] }, + "Could not undo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage() + ) + ); } // this must be the last past element in all the impacted resources! @@ -785,7 +810,15 @@ export class UndoRedoService implements IUndoRedoService { } } if (cannotUndoDueToResources.length > 0) { - return this._tryToSplitAndUndo(strResource, element, null, nls.localize('cannotWorkspaceUndoDueToChanges', "Could not undo '{0}' across all files because changes were made to {1}", element.label, cannotUndoDueToResources.join(', '))); + return this._tryToSplitAndUndo( + strResource, + element, + null, + nls.localize( + { key: 'cannotWorkspaceUndoDueToChanges', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] }, + "Could not undo '{0}' across all files because changes were made to {1}", element.label, cannotUndoDueToResources.join(', ') + ) + ); } const cannotLockDueToResources: string[] = []; @@ -795,12 +828,28 @@ export class UndoRedoService implements IUndoRedoService { } } if (cannotLockDueToResources.length > 0) { - return this._tryToSplitAndUndo(strResource, element, null, nls.localize('cannotWorkspaceUndoDueToInProgressUndoRedo', "Could not undo '{0}' across all files because there is already an undo or redo operation running on {1}", element.label, cannotLockDueToResources.join(', '))); + return this._tryToSplitAndUndo( + strResource, + element, + null, + nls.localize( + { key: 'cannotWorkspaceUndoDueToInProgressUndoRedo', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] }, + "Could not undo '{0}' across all files because there is already an undo or redo operation running on {1}", element.label, cannotLockDueToResources.join(', ') + ) + ); } // check if new stack elements were added in the meantime... if (!editStackSnapshot.isValid()) { - return this._tryToSplitAndUndo(strResource, element, null, nls.localize('cannotWorkspaceUndoDueToInMeantimeUndoRedo', "Could not undo '{0}' across all files because an undo or redo operation occurred in the meantime", element.label)); + return this._tryToSplitAndUndo( + strResource, + element, + null, + nls.localize( + { key: 'cannotWorkspaceUndoDueToInMeantimeUndoRedo', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] }, + "Could not undo '{0}' across all files because an undo or redo operation occurred in the meantime", element.label + ) + ); } return null; @@ -881,7 +930,10 @@ export class UndoRedoService implements IUndoRedoService { return; } if (editStack.locked) { - const message = nls.localize('cannotResourceUndoDueToInProgressUndoRedo', "Could not undo '{0}' because there is already an undo or redo operation running.", element.label); + const message = nls.localize( + { key: 'cannotResourceUndoDueToInProgressUndoRedo', comment: ['{0} is a label for an operation.'] }, + "Could not undo '{0}' because there is already an undo or redo operation running.", element.label + ); this._notificationService.info(message); return; } @@ -942,10 +994,26 @@ export class UndoRedoService implements IUndoRedoService { private _checkWorkspaceRedo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot, checkInvalidatedResources: boolean): WorkspaceVerificationError | null { if (element.removedResources) { - return this._tryToSplitAndRedo(strResource, element, element.removedResources, nls.localize('cannotWorkspaceRedo', "Could not redo '{0}' across all files. {1}", element.label, element.removedResources.createMessage())); + return this._tryToSplitAndRedo( + strResource, + element, + element.removedResources, + nls.localize( + { key: 'cannotWorkspaceRedo', comment: ['{0} is a label for an operation. {1} is another message.'] }, + "Could not redo '{0}' across all files. {1}", element.label, element.removedResources.createMessage() + ) + ); } if (checkInvalidatedResources && element.invalidatedResources) { - return this._tryToSplitAndRedo(strResource, element, element.invalidatedResources, nls.localize('cannotWorkspaceRedo', "Could not redo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage())); + return this._tryToSplitAndRedo( + strResource, + element, + element.invalidatedResources, + nls.localize( + { key: 'cannotWorkspaceRedo', comment: ['{0} is a label for an operation. {1} is another message.'] }, + "Could not redo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage() + ) + ); } // this must be the last future element in all the impacted resources! @@ -956,7 +1024,15 @@ export class UndoRedoService implements IUndoRedoService { } } if (cannotRedoDueToResources.length > 0) { - return this._tryToSplitAndRedo(strResource, element, null, nls.localize('cannotWorkspaceRedoDueToChanges', "Could not redo '{0}' across all files because changes were made to {1}", element.label, cannotRedoDueToResources.join(', '))); + return this._tryToSplitAndRedo( + strResource, + element, + null, + nls.localize( + { key: 'cannotWorkspaceRedoDueToChanges', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] }, + "Could not redo '{0}' across all files because changes were made to {1}", element.label, cannotRedoDueToResources.join(', ') + ) + ); } const cannotLockDueToResources: string[] = []; @@ -966,12 +1042,28 @@ export class UndoRedoService implements IUndoRedoService { } } if (cannotLockDueToResources.length > 0) { - return this._tryToSplitAndRedo(strResource, element, null, nls.localize('cannotWorkspaceRedoDueToInProgressUndoRedo', "Could not redo '{0}' across all files because there is already an undo or redo operation running on {1}", element.label, cannotLockDueToResources.join(', '))); + return this._tryToSplitAndRedo( + strResource, + element, + null, + nls.localize( + { key: 'cannotWorkspaceRedoDueToInProgressUndoRedo', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] }, + "Could not redo '{0}' across all files because there is already an undo or redo operation running on {1}", element.label, cannotLockDueToResources.join(', ') + ) + ); } // check if new stack elements were added in the meantime... if (!editStackSnapshot.isValid()) { - return this._tryToSplitAndRedo(strResource, element, null, nls.localize('cannotWorkspaceRedoDueToInMeantimeUndoRedo', "Could not redo '{0}' across all files because an undo or redo operation occurred in the meantime", element.label)); + return this._tryToSplitAndRedo( + strResource, + element, + null, + nls.localize( + { key: 'cannotWorkspaceRedoDueToInMeantimeUndoRedo', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] }, + "Could not redo '{0}' across all files because an undo or redo operation occurred in the meantime", element.label + ) + ); } return null; @@ -1015,7 +1107,10 @@ export class UndoRedoService implements IUndoRedoService { return; } if (editStack.locked) { - const message = nls.localize('cannotResourceRedoDueToInProgressUndoRedo', "Could not redo '{0}' because there is already an undo or redo operation running.", element.label); + const message = nls.localize( + { key: 'cannotResourceRedoDueToInProgressUndoRedo', comment: ['{0} is a label for an operation.'] }, + "Could not redo '{0}' because there is already an undo or redo operation running.", element.label + ); this._notificationService.info(message); return; } diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 4aa6495562c..3457eabe31e 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -53,20 +53,41 @@ function isSyncData(thing: any): thing is ISyncData { return false; } -export interface IMergableResourcePreview extends IBaseResourcePreview { +export interface IResourcePreview { + + readonly remoteResource: URI; readonly remoteContent: string | null; + readonly remoteChange: Change; + + readonly localResource: URI; readonly localContent: string | null; - readonly previewContent: string | null; - readonly acceptedContent: string | null; + readonly localChange: Change; + + readonly previewResource: URI; + readonly acceptedResource: URI; +} + +export interface IAcceptResult { + readonly content: string | null; + readonly localChange: Change; + readonly remoteChange: Change; +} + +export interface IMergeResult extends IAcceptResult { readonly hasConflicts: boolean; } -export type IResourcePreview = Omit; +interface IEditableResourcePreview extends IBaseResourcePreview, IResourcePreview { + localChange: Change; + remoteChange: Change; + mergeState: MergeState; + acceptResult?: IAcceptResult; +} -export interface ISyncResourcePreview extends IBaseSyncResourcePreview { +interface ISyncResourcePreview extends IBaseSyncResourcePreview { readonly remoteUserData: IRemoteUserData; readonly lastSyncUserData: IRemoteUserData | null; - readonly resourcePreviews: IMergableResourcePreview[]; + readonly resourcePreviews: IEditableResourcePreview[]; } export abstract class AbstractSynchroniser extends Disposable { @@ -82,10 +103,10 @@ export abstract class AbstractSynchroniser extends Disposable { private _onDidChangStatus: Emitter = this._register(new Emitter()); readonly onDidChangeStatus: Event = this._onDidChangStatus.event; - private _conflicts: IMergableResourcePreview[] = []; - get conflicts(): IMergableResourcePreview[] { return this._conflicts; } - private _onDidChangeConflicts: Emitter = this._register(new Emitter()); - readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; + private _conflicts: IBaseResourcePreview[] = []; + get conflicts(): IBaseResourcePreview[] { return this._conflicts; } + private _onDidChangeConflicts: Emitter = this._register(new Emitter()); + readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; private readonly localChangeTriggerScheduler = new RunOnceScheduler(() => this.doTriggerLocalChange(), 50); private readonly _onDidChangeLocal: Emitter = this._register(new Emitter()); @@ -99,7 +120,7 @@ export abstract class AbstractSynchroniser extends Disposable { constructor( readonly resource: SyncResource, @IFileService protected readonly fileService: IFileService, - @IEnvironmentService environmentService: IEnvironmentService, + @IEnvironmentService protected readonly environmentService: IEnvironmentService, @IStorageService storageService: IStorageService, @IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncBackupStoreService protected readonly userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @@ -162,53 +183,6 @@ export abstract class AbstractSynchroniser extends Disposable { } } - async pull(): Promise { - if (!this.isEnabled()) { - this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling ${this.syncResourceLogLabel.toLowerCase()} as it is disabled.`); - return; - } - - await this.stop(); - - try { - this.logService.info(`${this.syncResourceLogLabel}: Started pulling ${this.syncResourceLogLabel.toLowerCase()}...`); - this.setStatus(SyncStatus.Syncing); - - const lastSyncUserData = await this.getLastSyncUserData(); - const remoteUserData = await this.getRemoteUserData(lastSyncUserData); - const preview = await this.generatePullPreview(remoteUserData, lastSyncUserData, CancellationToken.None); - - await this.applyPreview(remoteUserData, lastSyncUserData, preview, false); - this.logService.info(`${this.syncResourceLogLabel}: Finished pulling ${this.syncResourceLogLabel.toLowerCase()}.`); - } finally { - this.setStatus(SyncStatus.Idle); - } - } - - async push(): Promise { - if (!this.isEnabled()) { - this.logService.info(`${this.syncResourceLogLabel}: Skipped pushing ${this.syncResourceLogLabel.toLowerCase()} as it is disabled.`); - return; - } - - this.stop(); - - try { - this.logService.info(`${this.syncResourceLogLabel}: Started pushing ${this.syncResourceLogLabel.toLowerCase()}...`); - this.setStatus(SyncStatus.Syncing); - - const lastSyncUserData = await this.getLastSyncUserData(); - const remoteUserData = await this.getRemoteUserData(lastSyncUserData); - const preview = await this.generatePushPreview(remoteUserData, lastSyncUserData, CancellationToken.None); - - await this.applyPreview(remoteUserData, lastSyncUserData, preview, true); - this.logService.info(`${this.syncResourceLogLabel}: Finished pushing ${this.syncResourceLogLabel.toLowerCase()}.`); - - } finally { - this.setStatus(SyncStatus.Idle); - } - } - async sync(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise { await this._sync(manifest, true, headers); } @@ -292,8 +266,20 @@ export abstract class AbstractSynchroniser extends Disposable { this.setStatus(SyncStatus.Syncing); const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getLatestRemoteUserData(null, lastSyncUserData); - const preview = await this.generateReplacePreview(syncData, remoteUserData, lastSyncUserData); - await this.applyPreview(remoteUserData, lastSyncUserData, preview, false); + + /* use replace sync data */ + const resourcePreviewResults = await this.generateSyncPreview({ ref: remoteUserData.ref, syncData }, lastSyncUserData, CancellationToken.None); + + const resourcePreviews: [IResourcePreview, IAcceptResult][] = []; + for (const resourcePreviewResult of resourcePreviewResults) { + /* Accept remote resource */ + const acceptResult: IAcceptResult = await this.getAcceptResult(resourcePreviewResult, resourcePreviewResult.remoteResource, undefined, CancellationToken.None); + /* compute remote change */ + const { remoteChange } = await this.getAcceptResult(resourcePreviewResult, resourcePreviewResult.previewResource, resourcePreviewResult.remoteContent, CancellationToken.None); + resourcePreviews.push([resourcePreviewResult, { ...acceptResult, remoteChange: remoteChange !== Change.None ? remoteChange : Change.Modified }]); + } + + await this.applyResult(remoteUserData, lastSyncUserData, resourcePreviews, false); this.logService.info(`${this.syncResourceLogLabel}: Finished resetting ${this.resource.toLowerCase()}.`); } finally { this.setStatus(SyncStatus.Idle); @@ -384,41 +370,48 @@ export abstract class AbstractSynchroniser extends Disposable { } } - async accept(resource: URI, content: string): Promise { + async merge(resource: URI): Promise { await this.updateSyncResourcePreview(resource, async (resourcePreview) => { - const updatedResourcePreview = await this.updateResourcePreview(resourcePreview, resource, content); - return { - ...updatedResourcePreview, - mergeState: MergeState.Accepted - }; + const mergeResult = await this.getMergeResult(resourcePreview, CancellationToken.None); + await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(mergeResult?.content || '')); + const acceptResult: IAcceptResult | undefined = mergeResult && !mergeResult.hasConflicts + ? await this.getAcceptResult(resourcePreview, resourcePreview.previewResource, undefined, CancellationToken.None) + : undefined; + resourcePreview.acceptResult = acceptResult; + resourcePreview.mergeState = mergeResult.hasConflicts ? MergeState.Conflict : acceptResult ? MergeState.Accepted : MergeState.Preview; + resourcePreview.localChange = acceptResult ? acceptResult.localChange : mergeResult.localChange; + resourcePreview.remoteChange = acceptResult ? acceptResult.remoteChange : mergeResult.remoteChange; + return resourcePreview; }); return this.syncPreviewPromise; } - async merge(resource: URI): Promise { + async accept(resource: URI, content?: string | null): Promise { await this.updateSyncResourcePreview(resource, async (resourcePreview) => { - const updatedResourcePreview = await this.updateResourcePreview(resourcePreview, resourcePreview.previewResource, resourcePreview.previewContent || ''); - return { - ...updatedResourcePreview, - mergeState: resourcePreview.hasConflicts ? MergeState.Conflict : MergeState.Accepted - }; + const acceptResult = await this.getAcceptResult(resourcePreview, resource, content, CancellationToken.None); + resourcePreview.acceptResult = acceptResult; + resourcePreview.mergeState = MergeState.Accepted; + resourcePreview.localChange = acceptResult.localChange; + resourcePreview.remoteChange = acceptResult.remoteChange; + return resourcePreview; }); return this.syncPreviewPromise; } async discard(resource: URI): Promise { await this.updateSyncResourcePreview(resource, async (resourcePreview) => { - await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(resourcePreview.previewContent || '')); - const updatedResourcePreview = await this.updateResourcePreview(resourcePreview, resourcePreview.previewResource, resourcePreview.previewContent || ''); - return { - ...updatedResourcePreview, - mergeState: MergeState.Preview - }; + const mergeResult = await this.getMergeResult(resourcePreview, CancellationToken.None); + await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(mergeResult.content || '')); + resourcePreview.acceptResult = undefined; + resourcePreview.mergeState = MergeState.Preview; + resourcePreview.localChange = mergeResult.localChange; + resourcePreview.remoteChange = mergeResult.remoteChange; + return resourcePreview; }); return this.syncPreviewPromise; } - private async updateSyncResourcePreview(resource: URI, updateResourcePreview: (resourcePreview: IMergableResourcePreview) => Promise): Promise { + private async updateSyncResourcePreview(resource: URI, updateResourcePreview: (resourcePreview: IEditableResourcePreview) => Promise): Promise { if (!this.syncPreviewPromise) { return; } @@ -448,13 +441,6 @@ export abstract class AbstractSynchroniser extends Disposable { } } - protected async updateResourcePreview(resourcePreview: IResourcePreview, resource: URI, acceptedContent: string): Promise { - return { - ...resourcePreview, - acceptedContent - }; - } - private async doApply(force: boolean): Promise { if (!this.syncPreviewPromise) { return SyncStatus.Idle; @@ -473,7 +459,7 @@ export abstract class AbstractSynchroniser extends Disposable { } // apply preview - await this.applyPreview(preview.remoteUserData, preview.lastSyncUserData, preview.resourcePreviews, force); + await this.applyResult(preview.remoteUserData, preview.lastSyncUserData, preview.resourcePreviews.map(resourcePreview => ([resourcePreview, resourcePreview.acceptResult!])), force); // reset preview this.syncPreviewPromise = null; @@ -490,8 +476,8 @@ export abstract class AbstractSynchroniser extends Disposable { } catch (error) { /* Ignore */ } } - private updateConflicts(previews: IMergableResourcePreview[]): void { - const conflicts = previews.filter(p => p.mergeState === MergeState.Conflict); + private updateConflicts(resourcePreviews: IEditableResourcePreview[]): void { + const conflicts = resourcePreviews.filter(({ mergeState }) => mergeState === MergeState.Conflict); if (!equals(this._conflicts, conflicts, (a, b) => isEqual(a.previewResource, b.previewResource))) { this._conflicts = conflicts; this._onDidChangeConflicts.fire(conflicts); @@ -550,13 +536,13 @@ export abstract class AbstractSynchroniser extends Disposable { if (syncPreview) { for (const resourcePreview of syncPreview.resourcePreviews) { if (isEqual(resourcePreview.acceptedResource, uri)) { - return resourcePreview.acceptedContent || ''; + return resourcePreview.acceptResult ? resourcePreview.acceptResult.content : null; } if (isEqual(resourcePreview.remoteResource, uri)) { - return resourcePreview.remoteContent || ''; + return resourcePreview.remoteContent; } if (isEqual(resourcePreview.localResource, uri)) { - return resourcePreview.localContent || ''; + return resourcePreview.localContent; } } } @@ -575,22 +561,45 @@ export abstract class AbstractSynchroniser extends Disposable { // For preview, use remoteUserData if lastSyncUserData does not exists and last sync is from current machine const lastSyncUserDataForPreview = lastSyncUserData === null && isLastSyncFromCurrentMachine ? remoteUserData : lastSyncUserData; - const result = await this.generateSyncPreview(remoteUserData, lastSyncUserDataForPreview, token); + const resourcePreviewResults = await this.generateSyncPreview(remoteUserData, lastSyncUserDataForPreview, token); - const resourcePreviews: IMergableResourcePreview[] = []; - for (const resourcePreview of result) { - if (token.isCancellationRequested) { - break; + const resourcePreviews: IEditableResourcePreview[] = []; + for (const resourcePreviewResult of resourcePreviewResults) { + const acceptedResource = resourcePreviewResult.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }); + + /* No change -> Accept */ + if (resourcePreviewResult.localChange === Change.None && resourcePreviewResult.remoteChange === Change.None) { + resourcePreviews.push({ + ...resourcePreviewResult, + acceptedResource, + acceptResult: { content: null, localChange: Change.None, remoteChange: Change.None }, + mergeState: MergeState.Accepted + }); } - if (!apply) { - await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(resourcePreview.previewContent || '')); + + /* Changed -> Apply ? (Merge ? Conflict | Accept) : Preview */ + else { + /* Merge */ + const mergeResult = apply ? await this.getMergeResult(resourcePreviewResult, token) : undefined; + if (token.isCancellationRequested) { + break; + } + await this.fileService.writeFile(resourcePreviewResult.previewResource, VSBuffer.fromString(mergeResult?.content || '')); + + /* Conflict | Accept */ + const acceptResult = mergeResult && !mergeResult.hasConflicts + /* Accept if merged and there are no conflicts */ + ? await this.getAcceptResult(resourcePreviewResult, resourcePreviewResult.previewResource, undefined, token) + : undefined; + + resourcePreviews.push({ + ...resourcePreviewResult, + acceptResult, + mergeState: mergeResult?.hasConflicts ? MergeState.Conflict : acceptResult ? MergeState.Accepted : MergeState.Preview, + localChange: acceptResult ? acceptResult.localChange : mergeResult ? mergeResult.localChange : resourcePreviewResult.localChange, + remoteChange: acceptResult ? acceptResult.remoteChange : mergeResult ? mergeResult.remoteChange : resourcePreviewResult.remoteChange + }); } - resourcePreviews.push({ - ...resourcePreview, - mergeState: resourcePreview.localChange === Change.None && resourcePreview.remoteChange === Change.None ? MergeState.Accepted /* Mark previews with no changes as merged */ - : apply ? (resourcePreview.hasConflicts ? MergeState.Conflict : MergeState.Accepted) - : MergeState.Preview - }); } return { remoteUserData, lastSyncUserData, resourcePreviews, isLastSyncFromCurrentMachine }; @@ -643,7 +652,7 @@ export abstract class AbstractSynchroniser extends Disposable { } catch (error) { this.logService.error(error); } - throw new UserDataSyncError(localize('incompatible sync data', "Cannot parse sync data as it is not compatible with current version."), UserDataSyncErrorCode.IncompatibleRemoteContent, this.resource); + throw new UserDataSyncError(localize('incompatible sync data', "Cannot parse sync data as it is not compatible with the current version."), UserDataSyncErrorCode.IncompatibleRemoteContent, this.resource); } private async getUserData(refOrLastSyncData: string | IRemoteUserData | null): Promise { @@ -687,11 +696,10 @@ export abstract class AbstractSynchroniser extends Disposable { } protected abstract readonly version: number; - protected abstract generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise; - protected abstract generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise; - protected abstract generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise; protected abstract generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise; - protected abstract applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IResourcePreview[], forcePush: boolean): Promise; + protected abstract getMergeResult(resourcePreview: IResourcePreview, token: CancellationToken): Promise; + protected abstract getAcceptResult(resourcePreview: IResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise; + protected abstract applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, result: [IResourcePreview, IAcceptResult][], force: boolean): Promise; } export interface IFileResourcePreview extends IResourcePreview { diff --git a/src/vs/platform/userDataSync/common/extensionsMerge.ts b/src/vs/platform/userDataSync/common/extensionsMerge.ts index 381047368e0..1540037a7b9 100644 --- a/src/vs/platform/userDataSync/common/extensionsMerge.ts +++ b/src/vs/platform/userDataSync/common/extensionsMerge.ts @@ -205,7 +205,7 @@ function massageOutgoingExtension(extension: ISyncExtension, key: string): ISync export function getIgnoredExtensions(installed: ILocalExtension[], configurationService: IConfigurationService): string[] { const defaultIgnoredExtensions = installed.filter(i => i.isMachineScoped).map(i => i.identifier.id.toLowerCase()); - const value = (configurationService.getValue('sync.ignoredExtensions') || []).map(id => id.toLowerCase()); + const value = getConfiguredIgnoredExtensions(configurationService).map(id => id.toLowerCase()); const added: string[] = [], removed: string[] = []; if (Array.isArray(value)) { for (const key of value) { @@ -218,3 +218,15 @@ export function getIgnoredExtensions(installed: ILocalExtension[], configuration } return distinct([...defaultIgnoredExtensions, ...added,].filter(setting => removed.indexOf(setting) === -1)); } + +function getConfiguredIgnoredExtensions(configurationService: IConfigurationService): string[] { + let userValue = configurationService.inspect('settingsSync.ignoredExtensions').userValue; + if (userValue !== undefined) { + return userValue; + } + userValue = configurationService.inspect('sync.ignoredExtensions').userValue; + if (userValue !== undefined) { + return userValue; + } + return configurationService.getValue('settingsSync.ignoredExtensions') || []; +} diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 5101b59bf25..d0c714df48b 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, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { 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'; @@ -25,13 +25,17 @@ import { compare } from 'vs/base/common/strings'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { CancellationToken } from 'vs/base/common/cancellation'; -export interface IExtensionResourcePreview extends IResourcePreview { - readonly localExtensions: ISyncExtension[]; +interface IExtensionResourceMergeResult extends IAcceptResult { readonly added: ISyncExtension[]; readonly removed: IExtensionIdentifier[]; readonly updated: ISyncExtension[]; readonly remote: ISyncExtension[] | null; +} + +interface IExtensionResourcePreview extends IResourcePreview { + readonly localExtensions: ISyncExtension[]; readonly skippedExtensions: ISyncExtension[]; + readonly previewResult: IExtensionResourceMergeResult; } interface ILastSyncUserData extends IRemoteUserData { @@ -41,10 +45,14 @@ interface ILastSyncUserData extends IRemoteUserData { 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` }); + /* Version 3 - Introduce installed property to skip installing built in extensions + protected readonly version: number = 3; */ - protected readonly version: number = 3; + /* Version 4: Change settings from `sync.${setting}` to `settingsSync.{setting}` */ + protected readonly version: number = 4; + protected isEnabled(): boolean { return super.isEnabled() && this.extensionGalleryService.isEnabled(); } private readonly previewResource: URI = joinPath(this.syncPreviewFolder, 'extensions.json'); private readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }); @@ -75,47 +83,6 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse () => undefined, 500)(() => this.triggerLocalChange())); } - protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise { - const remoteExtensions = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null; - const pullPreview = await this.getPullPreview(remoteExtensions); - return [pullPreview]; - } - - protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise { - const remoteExtensions = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null; - const pushPreview = await this.getPushPreview(remoteExtensions); - return [pushPreview]; - } - - protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { - const installedExtensions = await this.extensionManagementService.getInstalled(); - const localExtensions = this.getLocalExtensions(installedExtensions); - const remoteExtensions = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null; - const syncExtensions = await this.parseAndMigrateExtensions(syncData); - const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService); - const mergeResult = merge(localExtensions, syncExtensions, localExtensions, [], ignoredExtensions); - const { added, removed, updated } = mergeResult; - return [{ - localResource: this.localResource, - localContent: this.format(localExtensions), - remoteResource: this.remoteResource, - remoteContent: remoteExtensions ? this.format(remoteExtensions) : null, - previewResource: this.previewResource, - previewContent: null, - acceptedResource: this.acceptedResource, - acceptedContent: null, - added, - removed, - updated, - remote: syncExtensions, - localExtensions, - skippedExtensions: [], - localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None, - remoteChange: Change.Modified, - hasConflicts: false, - }]; - } - protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null; const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : []; @@ -131,32 +98,125 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse this.logService.trace(`${this.syncResourceLogLabel}: Remote extensions does not exist. Synchronizing extensions for the first time.`); } - const mergeResult = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions); - const { added, removed, updated, remote } = mergeResult; - - return [{ - localResource: this.localResource, - localContent: this.format(localExtensions), - remoteResource: this.remoteResource, - remoteContent: remoteExtensions ? this.format(remoteExtensions) : null, - previewResource: this.previewResource, - previewContent: null, - acceptedResource: this.acceptedResource, - acceptedContent: null, + const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions); + const previewResult: IExtensionResourceMergeResult = { added, removed, updated, remote, - localExtensions, - skippedExtensions, + content: this.getPreviewContent(localExtensions, added, updated, removed), localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None, remoteChange: remote !== null ? Change.Modified : Change.None, - hasConflicts: false, + }; + + return [{ + skippedExtensions, + localResource: this.localResource, + localContent: this.format(localExtensions), + localExtensions, + remoteResource: this.remoteResource, + remoteContent: remoteExtensions ? this.format(remoteExtensions) : null, + previewResource: this.previewResource, + previewResult, + localChange: previewResult.localChange, + remoteChange: previewResult.remoteChange, + acceptedResource: this.acceptedResource, }]; } - protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, resourcePreviews: IExtensionResourcePreview[], force: boolean): Promise { - let { added, removed, updated, remote, skippedExtensions, localExtensions, localChange, remoteChange } = resourcePreviews[0]; + private getPreviewContent(localExtensions: ISyncExtension[], added: ISyncExtension[], updated: ISyncExtension[], removed: IExtensionIdentifier[]): string { + const preview: ISyncExtension[] = [...added, ...updated]; + + const idsOrUUIDs: Set = new Set(); + const addIdentifier = (identifier: IExtensionIdentifier) => { + idsOrUUIDs.add(identifier.id.toLowerCase()); + if (identifier.uuid) { + idsOrUUIDs.add(identifier.uuid); + } + }; + preview.forEach(({ identifier }) => addIdentifier(identifier)); + removed.forEach(addIdentifier); + + for (const localExtension of localExtensions) { + if (idsOrUUIDs.has(localExtension.identifier.id.toLowerCase()) || (localExtension.identifier.uuid && idsOrUUIDs.has(localExtension.identifier.uuid))) { + // skip + continue; + } + preview.push(localExtension); + } + + return this.format(preview); + } + + protected async getMergeResult(resourcePreview: IExtensionResourcePreview, token: CancellationToken): Promise { + return { ...resourcePreview.previewResult, hasConflicts: false }; + } + + protected async getAcceptResult(resourcePreview: IExtensionResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise { + + /* Accept local resource */ + if (isEqual(resource, this.localResource)) { + return this.acceptLocal(resourcePreview); + } + + /* Accept remote resource */ + if (isEqual(resource, this.remoteResource)) { + return this.acceptRemote(resourcePreview); + } + + /* Accept preview resource */ + if (isEqual(resource, this.previewResource)) { + return resourcePreview.previewResult; + } + + throw new Error(`Invalid Resource: ${resource.toString()}`); + } + + private async acceptLocal(resourcePreview: IExtensionResourcePreview): Promise { + const installedExtensions = await this.extensionManagementService.getInstalled(); + const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService); + const mergeResult = merge(resourcePreview.localExtensions, null, null, resourcePreview.skippedExtensions, ignoredExtensions); + const { added, removed, updated, remote } = mergeResult; + return { + content: resourcePreview.localContent, + added, + removed, + updated, + remote, + localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None, + remoteChange: remote !== null ? Change.Modified : Change.None, + }; + } + + private async acceptRemote(resourcePreview: IExtensionResourcePreview): Promise { + const installedExtensions = await this.extensionManagementService.getInstalled(); + const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService); + const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null; + if (remoteExtensions !== null) { + const mergeResult = merge(resourcePreview.localExtensions, remoteExtensions, resourcePreview.localExtensions, [], ignoredExtensions); + const { added, removed, updated, remote } = mergeResult; + return { + content: resourcePreview.remoteContent, + added, + removed, + updated, + remote, + localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None, + remoteChange: remote !== null ? Change.Modified : Change.None, + }; + } else { + return { + content: resourcePreview.remoteContent, + added: [], removed: [], updated: [], remote: null, + localChange: Change.None, + remoteChange: Change.None, + }; + } + } + + protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [IExtensionResourcePreview, IExtensionResourceMergeResult][], force: boolean): Promise { + let { skippedExtensions, localExtensions } = resourcePreviews[0][0]; + let { added, removed, updated, remote, localChange, remoteChange } = resourcePreviews[0][1]; if (localChange === Change.None && remoteChange === Change.None) { this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing extensions.`); @@ -183,102 +243,18 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } } - protected async updateResourcePreview(resourcePreview: IExtensionResourcePreview, resource: URI, acceptedContent: string): Promise { - if (isEqual(resource, this.localResource)) { - const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null; - return this.getPushPreview(remoteExtensions); - } - return { - ...resourcePreview, - acceptedContent, - hasConflicts: false, - localChange: Change.Modified, - remoteChange: Change.Modified, - }; - } - - private async getPullPreview(remoteExtensions: ISyncExtension[] | null): Promise { - const installedExtensions = await this.extensionManagementService.getInstalled(); - const localExtensions = this.getLocalExtensions(installedExtensions); - const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService); - const localResource = this.localResource; - const localContent = this.format(localExtensions); - const remoteResource = this.remoteResource; - const previewResource = this.previewResource; - const acceptedResource = this.acceptedResource; - const previewContent = null; - if (remoteExtensions !== null) { - const mergeResult = merge(localExtensions, remoteExtensions, localExtensions, [], ignoredExtensions); - const { added, removed, updated, remote } = mergeResult; - return { - localResource, - localContent, - remoteResource, - remoteContent: this.format(remoteExtensions), - previewResource, - previewContent, - acceptedResource, - acceptedContent: previewContent, - added, - removed, - updated, - remote, - localExtensions, - skippedExtensions: [], - localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None, - remoteChange: remote !== null ? Change.Modified : Change.None, - hasConflicts: false, - }; - } else { - return { - localResource, - localContent, - remoteResource, - remoteContent: null, - previewResource, - previewContent, - acceptedResource, - acceptedContent: previewContent, - added: [], removed: [], updated: [], remote: null, localExtensions, skippedExtensions: [], - localChange: Change.None, - remoteChange: Change.None, - hasConflicts: false, - }; - } - } - - private async getPushPreview(remoteExtensions: ISyncExtension[] | null): Promise { - const installedExtensions = await this.extensionManagementService.getInstalled(); - const localExtensions = this.getLocalExtensions(installedExtensions); - const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService); - const mergeResult = merge(localExtensions, null, null, [], ignoredExtensions); - const { added, removed, updated, remote } = mergeResult; - return { - localResource: this.localResource, - localContent: this.format(localExtensions), - remoteResource: this.remoteResource, - remoteContent: remoteExtensions ? this.format(remoteExtensions) : null, - previewResource: this.previewResource, - previewContent: null, - acceptedResource: this.acceptedResource, - acceptedContent: null, - added, - removed, - updated, - remote, - localExtensions, - skippedExtensions: [], - localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None, - remoteChange: remote !== null ? Change.Modified : Change.None, - hasConflicts: false, - }; - } - - async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { + async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> { return [{ resource: joinPath(uri, 'extensions.json'), comparableResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI }]; } async resolveContent(uri: URI): Promise { + if (isEqual(uri, ExtensionsSynchroniser.EXTENSIONS_DATA_URI)) { + const installedExtensions = await this.extensionManagementService.getInstalled(); + const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService); + const localExtensions = this.getLocalExtensions(installedExtensions).filter(e => !ignoredExtensions.some(id => areSameExtensions({ id }, e.identifier))); + return this.format(localExtensions); + } + if (isEqual(this.remoteResource, uri) || isEqual(this.localResource, uri) || isEqual(this.acceptedResource, uri)) { return this.resolvePreviewContent(uri); } diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index f04ae509afb..5929719d952 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -5,7 +5,7 @@ import { IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncResourceEnablementService, - IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, Change + IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, USER_DATA_SYNC_SCHEME, IRemoteUserData, Change } from 'vs/platform/userDataSync/common/userDataSync'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; @@ -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, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { 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'; @@ -30,11 +30,15 @@ import { CancellationToken } from 'vs/base/common/cancellation'; const argvStoragePrefx = 'globalState.argv.'; const argvProperties: string[] = ['locale']; -export interface IGlobalStateResourcePreview extends IResourcePreview { +interface IGlobalStateResourceMergeResult extends IAcceptResult { readonly local: { added: IStringDictionary, removed: string[], updated: IStringDictionary }; readonly remote: IStringDictionary | null; +} + +export interface IGlobalStateResourcePreview extends IResourcePreview { readonly skippedStorageKeys: string[]; readonly localUserData: IGlobalState; + readonly previewResult: IGlobalStateResourceMergeResult; } interface ILastSyncUserData extends IRemoteUserData { @@ -55,7 +59,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IUserDataSyncLogService logService: IUserDataSyncLogService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IEnvironmentService readonly environmentService: IEnvironmentService, @IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @ITelemetryService telemetryService: ITelemetryService, @IConfigurationService configurationService: IConfigurationService, @@ -76,43 +80,6 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs ); } - protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise { - const remoteContent = remoteUserData.syncData !== null ? remoteUserData.syncData.content : null; - const pullPreview = await this.getPullPreview(remoteContent, lastSyncUserData?.skippedStorageKeys || []); - return [pullPreview]; - } - - protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise { - const remoteContent = remoteUserData.syncData !== null ? remoteUserData.syncData.content : null; - const pushPreview = await this.getPushPreview(remoteContent); - return [pushPreview]; - } - - protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { - const localUserData = await this.getLocalGlobalState(); - const syncGlobalState: IGlobalState = JSON.parse(syncData.content); - const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; - const mergeResult = merge(localUserData.storage, syncGlobalState.storage, localUserData.storage, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService); - const { local, skipped } = mergeResult; - return [{ - localResource: this.localResource, - localContent: this.format(localUserData), - remoteResource: this.remoteResource, - remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null, - previewResource: this.previewResource, - previewContent: null, - acceptedResource: this.acceptedResource, - acceptedContent: null, - local, - remote: syncGlobalState.storage, - localUserData, - skippedStorageKeys: skipped, - localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None, - remoteChange: Change.Modified, - hasConflicts: false, - }]; - } - protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise { const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; const lastSyncGlobalState: IGlobalState | null = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null; @@ -125,30 +92,89 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs this.logService.trace(`${this.syncResourceLogLabel}: Remote ui state does not exist. Synchronizing ui state for the first time.`); } - const mergeResult = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService); - const { local, remote, skipped } = mergeResult; + const { local, remote, skipped } = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService); + const previewResult: IGlobalStateResourceMergeResult = { + content: null, + local, + remote, + localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None, + remoteChange: remote !== null ? Change.Modified : Change.None, + }; return [{ + skippedStorageKeys: skipped, localResource: this.localResource, localContent: this.format(localGloablState), + localUserData: localGloablState, remoteResource: this.remoteResource, remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null, previewResource: this.previewResource, - previewContent: null, + previewResult, + localChange: previewResult.localChange, + remoteChange: previewResult.remoteChange, acceptedResource: this.acceptedResource, - acceptedContent: null, - local, - remote, - localUserData: localGloablState, - skippedStorageKeys: skipped, - localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None, - remoteChange: remote !== null ? Change.Modified : Change.None, - hasConflicts: false, }]; } - protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, resourcePreviews: IGlobalStateResourcePreview[], force: boolean): Promise { - let { local, remote, localUserData, localChange, remoteChange, skippedStorageKeys } = resourcePreviews[0]; + protected async getMergeResult(resourcePreview: IGlobalStateResourcePreview, token: CancellationToken): Promise { + return { ...resourcePreview.previewResult, hasConflicts: false }; + } + + protected async getAcceptResult(resourcePreview: IGlobalStateResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise { + + /* Accept local resource */ + if (isEqual(resource, this.localResource)) { + return this.acceptLocal(resourcePreview); + } + + /* Accept remote resource */ + if (isEqual(resource, this.remoteResource)) { + return this.acceptRemote(resourcePreview); + } + + /* Accept preview resource */ + if (isEqual(resource, this.previewResource)) { + return resourcePreview.previewResult; + } + + throw new Error(`Invalid Resource: ${resource.toString()}`); + } + + private async acceptLocal(resourcePreview: IGlobalStateResourcePreview): Promise { + return { + content: resourcePreview.localContent, + local: { added: {}, removed: [], updated: {} }, + remote: resourcePreview.localUserData.storage, + localChange: Change.None, + remoteChange: Change.Modified, + }; + } + + private async acceptRemote(resourcePreview: IGlobalStateResourcePreview): Promise { + if (resourcePreview.remoteContent !== null) { + const remoteGlobalState: IGlobalState = JSON.parse(resourcePreview.remoteContent); + const { local, remote } = merge(resourcePreview.localUserData.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), resourcePreview.skippedStorageKeys, this.logService); + return { + content: resourcePreview.remoteContent, + local, + remote, + localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None, + remoteChange: remote !== null ? Change.Modified : Change.None, + }; + } else { + return { + content: resourcePreview.remoteContent, + local: { added: {}, removed: [], updated: {} }, + remote: null, + localChange: Change.None, + remoteChange: Change.None, + }; + } + } + + protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, resourcePreviews: [IGlobalStateResourcePreview, IGlobalStateResourceMergeResult][], force: boolean): Promise { + let { localUserData, skippedStorageKeys } = resourcePreviews[0][0]; + let { local, remote, localChange, remoteChange } = resourcePreviews[0][1]; if (localChange === Change.None && remoteChange === Change.None) { this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ui state.`); @@ -178,93 +204,16 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs } } - protected async updateResourcePreview(resourcePreview: IGlobalStateResourcePreview, resource: URI, acceptedContent: string): Promise { - if (isEqual(this.localResource, resource)) { - return this.getPushPreview(resourcePreview.remoteContent); - } - if (isEqual(this.remoteResource, resource)) { - return this.getPullPreview(resourcePreview.remoteContent, resourcePreview.skippedStorageKeys); - } - return resourcePreview; - } - - private async getPullPreview(remoteContent: string | null, skippedStorageKeys: string[]): Promise { - const localGlobalState = await this.getLocalGlobalState(); - const localResource = this.localResource; - const localContent = this.format(localGlobalState); - const remoteResource = this.remoteResource; - const previewResource = this.previewResource; - const acceptedResource = this.acceptedResource; - const previewContent = null; - if (remoteContent !== null) { - const remoteGlobalState: IGlobalState = JSON.parse(remoteContent); - const mergeResult = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), skippedStorageKeys, this.logService); - const { local, remote, skipped } = mergeResult; - return { - localResource, - localContent, - remoteResource, - remoteContent: this.format(remoteGlobalState), - previewResource, - previewContent, - acceptedResource, - acceptedContent: previewContent, - local, - remote, - localUserData: localGlobalState, - skippedStorageKeys: skipped, - localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None, - remoteChange: remote !== null ? Change.Modified : Change.None, - hasConflicts: false, - }; - } else { - return { - localResource, - localContent, - remoteResource, - remoteContent: null, - previewResource, - previewContent, - acceptedResource, - acceptedContent: previewContent, - local: { added: {}, removed: [], updated: {} }, - remote: null, - localUserData: localGlobalState, - skippedStorageKeys: [], - localChange: Change.None, - remoteChange: Change.None, - hasConflicts: false, - }; - } - } - - private async getPushPreview(remoteContent: string | null): Promise { - const localUserData = await this.getLocalGlobalState(); - const remoteGlobalState: IGlobalState = remoteContent ? JSON.parse(remoteContent) : null; - return { - localResource: this.localResource, - localContent: this.format(localUserData), - remoteResource: this.remoteResource, - remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null, - previewResource: this.previewResource, - previewContent: null, - acceptedResource: this.acceptedResource, - acceptedContent: null, - local: { added: {}, removed: [], updated: {} }, - remote: localUserData.storage, - localUserData, - skippedStorageKeys: [], - localChange: Change.None, - remoteChange: Change.Modified, - hasConflicts: false, - }; - } - - async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { + async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> { return [{ resource: joinPath(uri, 'globalState.json'), comparableResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI }]; } async resolveContent(uri: URI): Promise { + if (isEqual(uri, GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI)) { + const localGlobalState = await this.getLocalGlobalState(); + return this.format(localGlobalState); + } + if (isEqual(this.remoteResource, uri) || isEqual(this.localResource, uri) || isEqual(this.acceptedResource, uri)) { return this.resolvePreviewContent(uri); } diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index b0a93988b67..cfafec8c3ae 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -7,10 +7,9 @@ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platfo import { UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncResource, IUserDataSynchroniser, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, ISyncResourceHandle, - IRemoteUserData, ISyncData, Change + IRemoteUserData, Change } from 'vs/platform/userDataSync/common/userDataSync'; import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge'; -import { VSBuffer } from 'vs/base/common/buffer'; import { parse } from 'vs/base/common/json'; import { localize } from 'vs/nls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -19,7 +18,7 @@ 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, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { 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'; @@ -32,9 +31,14 @@ interface ISyncContent { all?: string; } +interface IKeybindingsResourcePreview extends IFileResourcePreview { + previewResult: IMergeResult; +} + export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser { - protected readonly version: number = 1; + /* Version 2: Change settings from `sync.${setting}` to `settingsSync.{setting}` */ + protected readonly version: number = 2; private readonly previewResource: URI = joinPath(this.syncPreviewFolder, 'keybindings.json'); private readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }); private readonly remoteResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }); @@ -55,67 +59,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem super(environmentService.keybindingsResource, SyncResource.Keybindings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); } - protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { - const fileContent = await this.getLocalFileContent(); - const previewContent = remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null; - - return [{ - localResource: this.localResource, - fileContent, - localContent: fileContent ? fileContent.value.toString() : null, - remoteResource: this.remoteResource, - remoteContent: previewContent, - previewResource: this.previewResource, - previewContent, - acceptedResource: this.acceptedResource, - acceptedContent: previewContent, - localChange: previewContent !== null ? Change.Modified : Change.None, - remoteChange: Change.None, - hasConflicts: false, - }]; - } - - protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { - const fileContent = await this.getLocalFileContent(); - const previewContent: string | null = fileContent ? fileContent.value.toString() : null; - - return [{ - localResource: this.localResource, - fileContent, - localContent: fileContent ? fileContent.value.toString() : null, - remoteResource: this.remoteResource, - remoteContent: remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null, - previewResource: this.previewResource, - previewContent, - acceptedResource: this.acceptedResource, - acceptedContent: previewContent, - localChange: Change.None, - remoteChange: previewContent !== null ? Change.Modified : Change.None, - hasConflicts: false, - }]; - } - - protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { - const fileContent = await this.getLocalFileContent(); - const previewContent = this.getKeybindingsContentFromSyncContent(syncData.content); - - return [{ - localResource: this.localResource, - fileContent, - localContent: fileContent ? fileContent.value.toString() : null, - remoteResource: this.remoteResource, - remoteContent: remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null, - previewResource: this.previewResource, - previewContent, - acceptedResource: this.acceptedResource, - acceptedContent: previewContent, - localChange: previewContent !== null ? Change.Modified : Change.None, - remoteChange: previewContent !== null ? Change.Modified : Change.None, - hasConflicts: false, - }]; - } - - protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { const remoteContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null; const lastSyncContent: string | null = lastSyncUserData && lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null; @@ -123,14 +67,15 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem const fileContent = await this.getLocalFileContent(); const formattingOptions = await this.getFormattingOptions(); - let previewContent: string | null = null; + let mergedContent: string | null = null; let hasLocalChanged: boolean = false; let hasRemoteChanged: boolean = false; let hasConflicts: boolean = false; if (remoteContent) { - const localContent: string = fileContent ? fileContent.value.toString() : '[]'; - if (!localContent.trim() || this.hasErrors(localContent)) { + let localContent: string = fileContent ? fileContent.value.toString() : '[]'; + localContent = localContent || '[]'; + if (this.hasErrors(localContent)) { throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource); } @@ -142,7 +87,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem const result = await merge(localContent, remoteContent, lastSyncContent, formattingOptions, this.userDataSyncUtilService); // Sync only if there are changes if (result.hasChanges) { - previewContent = result.mergeContent; + mergedContent = result.mergeContent; hasConflicts = result.hasConflicts; hasLocalChanged = hasConflicts || result.mergeContent !== localContent; hasRemoteChanged = hasConflicts || result.mergeContent !== remoteContent; @@ -153,66 +98,126 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem // First time syncing to remote else if (fileContent) { this.logService.trace(`${this.syncResourceLogLabel}: Remote keybindings does not exist. Synchronizing keybindings for the first time.`); - previewContent = fileContent.value.toString(); + mergedContent = fileContent.value.toString(); hasRemoteChanged = true; } - if (previewContent && !token.isCancellationRequested) { - await this.fileService.writeFile(this.previewResource, VSBuffer.fromString(previewContent)); - } - - return [{ - localResource: this.localResource, - fileContent, - localContent: fileContent ? fileContent.value.toString() : null, - remoteResource: this.remoteResource, - remoteContent, - previewResource: this.previewResource, - previewContent, - acceptedResource: this.acceptedResource, - acceptedContent: previewContent, - hasConflicts, + const previewResult: IMergeResult = { + content: mergedContent, localChange: hasLocalChanged ? fileContent ? Change.Modified : Change.Added : Change.None, remoteChange: hasRemoteChanged ? Change.Modified : Change.None, + hasConflicts + }; + + return [{ + fileContent, + localResource: this.localResource, + localContent: fileContent ? fileContent.value.toString() : null, + localChange: previewResult.localChange, + + remoteResource: this.remoteResource, + remoteContent, + remoteChange: previewResult.remoteChange, + + previewResource: this.previewResource, + previewResult, + acceptedResource: this.acceptedResource, }]; + } - protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], force: boolean): Promise { - let { fileContent, acceptedContent: content, localChange, remoteChange } = resourcePreviews[0]; + protected async getMergeResult(resourcePreview: IKeybindingsResourcePreview, token: CancellationToken): Promise { + return resourcePreview.previewResult; + } - if (content !== null) { - if (this.hasErrors(content)) { - throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource); + protected async getAcceptResult(resourcePreview: IKeybindingsResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise { + + /* Accept local resource */ + if (isEqual(resource, this.localResource)) { + return { + content: resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : null, + localChange: Change.None, + remoteChange: Change.Modified, + }; + } + + /* Accept remote resource */ + if (isEqual(resource, this.remoteResource)) { + return { + content: resourcePreview.remoteContent, + localChange: Change.Modified, + remoteChange: Change.None, + }; + } + + /* Accept preview resource */ + if (isEqual(resource, this.previewResource)) { + if (content === undefined) { + return { + content: resourcePreview.previewResult.content, + localChange: resourcePreview.previewResult.localChange, + remoteChange: resourcePreview.previewResult.remoteChange, + }; + } else { + return { + content, + localChange: Change.Modified, + remoteChange: Change.Modified, + }; } + } - if (localChange !== Change.None) { - this.logService.trace(`${this.syncResourceLogLabel}: Updating local keybindings...`); - if (fileContent) { - await this.backupLocal(this.toSyncContent(fileContent.value.toString(), null)); - } - await this.updateLocalFileContent(content, fileContent, force); - this.logService.info(`${this.syncResourceLogLabel}: Updated local keybindings`); - } + throw new Error(`Invalid Resource: ${resource.toString()}`); + } - if (remoteChange !== Change.None) { - this.logService.trace(`${this.syncResourceLogLabel}: Updating remote keybindings...`); - const remoteContents = this.toSyncContent(content, remoteUserData.syncData ? remoteUserData.syncData.content : null); - remoteUserData = await this.updateRemoteUserData(remoteContents, force ? null : remoteUserData.ref); - this.logService.info(`${this.syncResourceLogLabel}: Updated remote keybindings`); - } + protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [IKeybindingsResourcePreview, IAcceptResult][], force: boolean): Promise { + const { fileContent } = resourcePreviews[0][0]; + let { content, localChange, remoteChange } = resourcePreviews[0][1]; - // Delete the preview - try { - await this.fileService.del(this.previewResource); - } catch (e) { /* ignore */ } - } else { + if (localChange === Change.None && remoteChange === Change.None) { this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing keybindings.`); } + if (content !== null) { + content = content.trim(); + content = content || '[]'; + if (this.hasErrors(content)) { + throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource); + } + } + + if (localChange !== Change.None) { + this.logService.trace(`${this.syncResourceLogLabel}: Updating local keybindings...`); + if (fileContent) { + await this.backupLocal(this.toSyncContent(fileContent.value.toString(), null)); + } + await this.updateLocalFileContent(content || '[]', fileContent, force); + this.logService.info(`${this.syncResourceLogLabel}: Updated local keybindings`); + } + + if (remoteChange !== Change.None) { + this.logService.trace(`${this.syncResourceLogLabel}: Updating remote keybindings...`); + const remoteContents = this.toSyncContent(content || '[]', remoteUserData.syncData ? remoteUserData.syncData.content : null); + remoteUserData = await this.updateRemoteUserData(remoteContents, force ? null : remoteUserData.ref); + this.logService.info(`${this.syncResourceLogLabel}: Updated remote keybindings`); + } + + // Delete the preview + try { + await this.fileService.del(this.previewResource); + } catch (e) { /* ignore */ } + if (lastSyncUserData?.ref !== remoteUserData.ref) { this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized keybindings...`); - const lastSyncContent = content !== null || fileContent !== null ? this.toSyncContent(content !== null ? content : fileContent!.value.toString(), null) : null; - await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: lastSyncContent ? { version: remoteUserData.syncData!.version, machineId: remoteUserData.syncData!.machineId, content: lastSyncContent } : null }); + const lastSyncContent = content !== null ? this.toSyncContent(content, null) : null; + await this.updateLastSyncUserData({ + ref: remoteUserData.ref, + syncData: lastSyncContent ? { + version: remoteUserData.syncData ? remoteUserData.syncData.version : this.version, + machineId: remoteUserData.syncData!.machineId, + content: lastSyncContent + } : null + }); this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized keybindings`); } @@ -235,8 +240,9 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem return false; } - async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { - return [{ resource: joinPath(uri, 'keybindings.json'), comparableResource: this.file }]; + async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> { + const comparableResource = (await this.fileService.exists(this.file)) ? this.file : this.localResource; + return [{ resource: joinPath(uri, 'keybindings.json'), comparableResource }]; } async resolveContent(uri: URI): Promise { @@ -263,7 +269,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem getKeybindingsContentFromSyncContent(syncContent: string): string | null { try { const parsed = JSON.parse(syncContent); - if (!this.configurationService.getValue('sync.keybindingsPerPlatform')) { + if (!this.syncKeybindingsPerPlatform()) { return isUndefined(parsed.all) ? null : parsed.all; } switch (OS) { @@ -287,7 +293,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem } catch (e) { this.logService.error(e); } - if (!this.configurationService.getValue('sync.keybindingsPerPlatform')) { + if (!this.syncKeybindingsPerPlatform()) { parsed.all = keybindingsContent; } else { delete parsed.all; @@ -306,4 +312,16 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem return JSON.stringify(parsed); } + private syncKeybindingsPerPlatform(): boolean { + let userValue = this.configurationService.inspect('settingsSync.keybindingsPerPlatform').userValue; + if (userValue !== undefined) { + return userValue; + } + userValue = this.configurationService.inspect('sync.keybindingsPerPlatform').userValue; + if (userValue !== undefined) { + return userValue; + } + return this.configurationService.getValue('settingsSync.keybindingsPerPlatform'); + } + } diff --git a/src/vs/platform/userDataSync/common/settingsMerge.ts b/src/vs/platform/userDataSync/common/settingsMerge.ts index d05975d3c94..ad6800c6cbb 100644 --- a/src/vs/platform/userDataSync/common/settingsMerge.ts +++ b/src/vs/platform/userDataSync/common/settingsMerge.ts @@ -23,12 +23,9 @@ export interface IMergeResult { export function getIgnoredSettings(defaultIgnoredSettings: string[], configurationService: IConfigurationService, settingsContent?: string): string[] { let value: string[] = []; if (settingsContent) { - const setting = parse(settingsContent); - if (setting) { - value = setting['sync.ignoredSettings']; - } + value = getIgnoredSettingsFromContent(settingsContent); } else { - value = configurationService.getValue('sync.ignoredSettings'); + value = getIgnoredSettingsFromConfig(configurationService); } const added: string[] = [], removed: string[] = [...getDisallowedIgnoredSettings()]; if (Array.isArray(value)) { @@ -43,6 +40,22 @@ export function getIgnoredSettings(defaultIgnoredSettings: string[], configurati return distinct([...defaultIgnoredSettings, ...added,].filter(setting => removed.indexOf(setting) === -1)); } +function getIgnoredSettingsFromConfig(configurationService: IConfigurationService): string[] { + let userValue = configurationService.inspect('settingsSync.ignoredSettings').userValue; + if (userValue !== undefined) { + return userValue; + } + userValue = configurationService.inspect('sync.ignoredSettings').userValue; + if (userValue !== undefined) { + return userValue; + } + return configurationService.getValue('settingsSync.ignoredSettings') || []; +} + +function getIgnoredSettingsFromContent(settingsContent: string): string[] { + const parsed = parse(settingsContent); + return parsed ? parsed['settingsSync.ignoredSettings'] || parsed['sync.ignoredSettings'] || [] : []; +} export function updateIgnoredSettings(targetContent: string, sourceContent: string, ignoredSettings: string[], formattingOptions: FormattingOptions): string { if (ignoredSettings.length) { diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index a8579c8f5de..cbba5c25c3d 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, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { 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'; @@ -26,6 +26,10 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { Edit } from 'vs/base/common/jsonFormatter'; import { setProperty, applyEdits } from 'vs/base/common/jsonEdit'; +interface ISettingsResourcePreview extends IFileResourcePreview { + previewResult: IMergeResult; +} + export interface ISettingsSyncContent { settings: string; } @@ -38,11 +42,12 @@ function isSettingsSyncContent(thing: any): thing is ISettingsSyncContent { export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser { - protected readonly version: number = 1; - private readonly previewResource: URI = joinPath(this.syncPreviewFolder, 'settings.json'); - private readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }); - private readonly remoteResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }); - private readonly acceptedResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }); + /* Version 2: Change settings from `sync.${setting}` to `settingsSync.{setting}` */ + protected readonly version: number = 2; + readonly previewResource: URI = joinPath(this.syncPreviewFolder, 'settings.json'); + readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }); + readonly remoteResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }); + readonly acceptedResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }); constructor( @IFileService fileService: IFileService, @@ -60,112 +65,25 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); } - protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { - - const fileContent = await this.getLocalFileContent(); - const formatUtils = await this.getFormattingOptions(); - const ignoredSettings = await this.getIgnoredSettings(); - const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); - - let previewContent: string | null = null; - if (remoteSettingsSyncContent !== null) { - // Update ignored settings from local file content - previewContent = updateIgnoredSettings(remoteSettingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils); - } - - return [{ - localResource: this.localResource, - fileContent, - localContent: fileContent ? fileContent.value.toString() : null, - remoteResource: this.remoteResource, - remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null, - previewResource: this.previewResource, - previewContent, - acceptedResource: this.acceptedResource, - acceptedContent: previewContent, - localChange: previewContent !== null ? Change.Modified : Change.None, - remoteChange: Change.None, - hasConflicts: false, - }]; - } - - protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { - - const fileContent = await this.getLocalFileContent(); - const formatUtils = await this.getFormattingOptions(); - const ignoredSettings = await this.getIgnoredSettings(); - const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); - - let previewContent: string | null = null; - if (fileContent !== null) { - // Remove ignored settings - previewContent = updateIgnoredSettings(fileContent.value.toString(), '{}', ignoredSettings, formatUtils); - } - - return [{ - localResource: this.localResource, - fileContent, - localContent: fileContent ? fileContent.value.toString() : null, - remoteResource: this.remoteResource, - remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null, - previewResource: this.previewResource, - previewContent, - acceptedResource: this.acceptedResource, - acceptedContent: previewContent, - localChange: Change.None, - remoteChange: previewContent !== null ? Change.Modified : Change.None, - hasConflicts: false, - }]; - } - - protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { - - const fileContent = await this.getLocalFileContent(); - const formatUtils = await this.getFormattingOptions(); - const ignoredSettings = await this.getIgnoredSettings(); - - let previewContent: string | null = null; - const settingsSyncContent = this.parseSettingsSyncContent(syncData.content); - const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); - if (settingsSyncContent) { - previewContent = updateIgnoredSettings(settingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils); - } - - return [{ - localResource: this.localResource, - fileContent, - localContent: fileContent ? fileContent.value.toString() : null, - remoteResource: this.remoteResource, - remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null, - previewResource: this.previewResource, - previewContent, - acceptedResource: this.acceptedResource, - acceptedContent: previewContent, - localChange: previewContent !== null ? Change.Modified : Change.None, - remoteChange: previewContent !== null ? Change.Modified : Change.None, - hasConflicts: false, - }]; - } - - protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { const fileContent = await this.getLocalFileContent(); const formattingOptions = await this.getFormattingOptions(); const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); const lastSettingsSyncContent: ISettingsSyncContent | null = lastSyncUserData ? this.getSettingsSyncContent(lastSyncUserData) : null; const ignoredSettings = await this.getIgnoredSettings(); - let acceptedContent: string | null = null; - let previewContent: string | null = null; + let mergedContent: string | null = null; let hasLocalChanged: boolean = false; let hasRemoteChanged: boolean = false; let hasConflicts: boolean = false; if (remoteSettingsSyncContent) { - const localContent: string = fileContent ? fileContent.value.toString() : '{}'; + let localContent: string = fileContent ? fileContent.value.toString().trim() : '{}'; + localContent = localContent || '{}'; this.validateContent(localContent); this.logService.trace(`${this.syncResourceLogLabel}: Merging remote settings with local settings...`); const result = merge(localContent, remoteSettingsSyncContent.settings, lastSettingsSyncContent ? lastSettingsSyncContent.settings : null, ignoredSettings, [], formattingOptions); - acceptedContent = result.localContent || result.remoteContent; + mergedContent = result.localContent || result.remoteContent; hasLocalChanged = result.localContent !== null; hasRemoteChanged = result.remoteContent !== null; hasConflicts = result.hasConflicts; @@ -174,75 +92,127 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement // First time syncing to remote else if (fileContent) { this.logService.trace(`${this.syncResourceLogLabel}: Remote settings does not exist. Synchronizing settings for the first time.`); - acceptedContent = fileContent.value.toString(); + mergedContent = fileContent.value.toString(); hasRemoteChanged = true; } - if (acceptedContent && !token.isCancellationRequested) { - // Remove the ignored settings from the preview. - previewContent = updateIgnoredSettings(acceptedContent, '{}', ignoredSettings, formattingOptions); - } + const previewResult = { + content: mergedContent, + localChange: hasLocalChanged ? Change.Modified : Change.None, + remoteChange: hasRemoteChanged ? Change.Modified : Change.None, + hasConflicts + }; return [{ - localResource: this.localResource, fileContent, + localResource: this.localResource, localContent: fileContent ? fileContent.value.toString() : null, + localChange: previewResult.localChange, + remoteResource: this.remoteResource, remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null, + remoteChange: previewResult.remoteChange, + previewResource: this.previewResource, - previewContent, + previewResult, acceptedResource: this.acceptedResource, - acceptedContent, - localChange: hasLocalChanged ? fileContent ? Change.Modified : Change.Added : Change.None, - remoteChange: hasRemoteChanged ? Change.Modified : Change.None, - hasConflicts, }]; } - protected async updateResourcePreview(resourcePreview: IFileResourcePreview, resource: URI, acceptedContent: string): Promise { - if (isEqual(resource, this.previewResource) || isEqual(resource, this.remoteResource)) { - const formatUtils = await this.getFormattingOptions(); - // Add ignored settings from local file content - const ignoredSettings = await this.getIgnoredSettings(); - acceptedContent = updateIgnoredSettings(acceptedContent, resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : '{}', ignoredSettings, formatUtils); - } - return super.updateResourcePreview(resourcePreview, resource, acceptedContent) as Promise; + protected async getMergeResult(resourcePreview: ISettingsResourcePreview, token: CancellationToken): Promise { + const formatUtils = await this.getFormattingOptions(); + const ignoredSettings = await this.getIgnoredSettings(); + return { + ...resourcePreview.previewResult, + + // remove ignored settings from the preview content + content: resourcePreview.previewResult.content ? updateIgnoredSettings(resourcePreview.previewResult.content, '{}', ignoredSettings, formatUtils) : null + }; } - protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], force: boolean): Promise { - let { fileContent, acceptedContent: content, localChange, remoteChange } = resourcePreviews[0]; + protected async getAcceptResult(resourcePreview: ISettingsResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise { - if (content !== null) { + const formattingOptions = await this.getFormattingOptions(); + const ignoredSettings = await this.getIgnoredSettings(); - this.validateContent(content); + /* Accept local resource */ + if (isEqual(resource, this.localResource)) { + return { + /* Remove ignored settings */ + content: resourcePreview.fileContent ? updateIgnoredSettings(resourcePreview.fileContent.value.toString(), '{}', ignoredSettings, formattingOptions) : null, + localChange: Change.None, + remoteChange: Change.Modified, + }; + } - if (localChange !== Change.None) { - this.logService.trace(`${this.syncResourceLogLabel}: Updating local settings...`); - if (fileContent) { - await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(fileContent.value.toString()))); - } - await this.updateLocalFileContent(content, fileContent, force); - this.logService.info(`${this.syncResourceLogLabel}: Updated local settings`); - } - if (remoteChange !== Change.None) { - const formatUtils = await this.getFormattingOptions(); - // Update ignored settings from remote - const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); - const ignoredSettings = await this.getIgnoredSettings(content); - content = updateIgnoredSettings(content, remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : '{}', ignoredSettings, formatUtils); - this.logService.trace(`${this.syncResourceLogLabel}: Updating remote settings...`); - remoteUserData = await this.updateRemoteUserData(JSON.stringify(this.toSettingsSyncContent(content)), force ? null : remoteUserData.ref); - this.logService.info(`${this.syncResourceLogLabel}: Updated remote settings`); + /* Accept remote resource */ + if (isEqual(resource, this.remoteResource)) { + return { + /* Update ignored settings from local file content */ + content: resourcePreview.remoteContent !== null ? updateIgnoredSettings(resourcePreview.remoteContent, resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : '{}', ignoredSettings, formattingOptions) : null, + localChange: Change.Modified, + remoteChange: Change.None, + }; + } + + /* Accept preview resource */ + if (isEqual(resource, this.previewResource)) { + if (content === undefined) { + return { + content: resourcePreview.previewResult.content, + localChange: resourcePreview.previewResult.localChange, + remoteChange: resourcePreview.previewResult.remoteChange, + }; + } else { + return { + /* Add ignored settings from local file content */ + content: content !== null ? updateIgnoredSettings(content, resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : '{}', ignoredSettings, formattingOptions) : null, + localChange: Change.Modified, + remoteChange: Change.Modified, + }; } + } - // Delete the preview - try { - await this.fileService.del(this.previewResource); - } catch (e) { /* ignore */ } - } else { + throw new Error(`Invalid Resource: ${resource.toString()}`); + } + + protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [ISettingsResourcePreview, IAcceptResult][], force: boolean): Promise { + const { fileContent } = resourcePreviews[0][0]; + let { content, localChange, remoteChange } = resourcePreviews[0][1]; + + if (localChange === Change.None && remoteChange === Change.None) { this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing settings.`); } + content = content ? content.trim() : '{}'; + content = content || '{}'; + this.validateContent(content); + + if (localChange !== Change.None) { + this.logService.trace(`${this.syncResourceLogLabel}: Updating local settings...`); + if (fileContent) { + await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(fileContent.value.toString()))); + } + await this.updateLocalFileContent(content, fileContent, force); + this.logService.info(`${this.syncResourceLogLabel}: Updated local settings`); + } + + if (remoteChange !== Change.None) { + const formatUtils = await this.getFormattingOptions(); + // Update ignored settings from remote + const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); + const ignoredSettings = await this.getIgnoredSettings(content); + content = updateIgnoredSettings(content, remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : '{}', ignoredSettings, formatUtils); + this.logService.trace(`${this.syncResourceLogLabel}: Updating remote settings...`); + remoteUserData = await this.updateRemoteUserData(JSON.stringify(this.toSettingsSyncContent(content)), force ? null : remoteUserData.ref); + this.logService.info(`${this.syncResourceLogLabel}: Updated remote settings`); + } + + // Delete the preview + try { + await this.fileService.del(this.previewResource); + } catch (e) { /* ignore */ } + if (lastSyncUserData?.ref !== remoteUserData.ref) { this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized settings...`); await this.updateLastSyncUserData(remoteUserData); @@ -267,8 +237,9 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement return false; } - async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { - return [{ resource: joinPath(uri, 'settings.json'), comparableResource: this.file }]; + async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> { + const comparableResource = (await this.fileService.exists(this.file)) ? this.file : this.localResource; + return [{ resource: joinPath(uri, 'settings.json'), comparableResource }]; } async resolveContent(uri: URI): Promise { @@ -297,7 +268,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement protected async resolvePreviewContent(resource: URI): Promise { let content = await super.resolvePreviewContent(resource); - if (content !== null) { + if (content) { const formatUtils = await this.getFormattingOptions(); // remove ignored settings from the preview content const ignoredSettings = await this.getIgnoredSettings(); diff --git a/src/vs/platform/userDataSync/common/snippetsSync.ts b/src/vs/platform/userDataSync/common/snippetsSync.ts index 3842f4513b6..f00332c395a 100644 --- a/src/vs/platform/userDataSync/common/snippetsSync.ts +++ b/src/vs/platform/userDataSync/common/snippetsSync.ts @@ -10,17 +10,25 @@ 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, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { 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'; import { joinPath, extname, relativePath, isEqualOrParent, basename, dirname } from 'vs/base/common/resources'; import { VSBuffer } from 'vs/base/common/buffer'; -import { merge, IMergeResult, areSame } from 'vs/platform/userDataSync/common/snippetsMerge'; +import { merge, IMergeResult as ISnippetsMergeResult, areSame } from 'vs/platform/userDataSync/common/snippetsMerge'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { deepClone } from 'vs/base/common/objects'; +interface ISnippetsResourcePreview extends IFileResourcePreview { + previewResult: IMergeResult; +} + +interface ISnippetsAcceptedResourcePreview extends IFileResourcePreview { + acceptResult: IAcceptResult; +} + export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { protected readonly version: number = 1; @@ -51,36 +59,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD this.triggerLocalChange(); } - protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { - const resourcePreviews: IFileResourcePreview[] = []; - if (remoteUserData.syncData !== null) { - const local = await this.getSnippetsFileContents(); - const localSnippets = this.toSnippetsContents(local); - const remoteSnippets = this.parseSnippets(remoteUserData.syncData); - const mergeResult = merge(localSnippets, remoteSnippets, localSnippets); - resourcePreviews.push(...this.getResourcePreviews(mergeResult, local, remoteSnippets)); - } - return resourcePreviews; - } - - protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { - const local = await this.getSnippetsFileContents(); - const localSnippets = this.toSnippetsContents(local); - const mergeResult = merge(localSnippets, null, null); - const resourcePreviews = this.getResourcePreviews(mergeResult, local, {}); - return resourcePreviews; - } - - protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { - const local = await this.getSnippetsFileContents(); - const localSnippets = this.toSnippetsContents(local); - const snippets = this.parseSnippets(syncData); - const mergeResult = merge(localSnippets, snippets, localSnippets); - const resourcePreviews = this.getResourcePreviews(mergeResult, local, snippets); - return resourcePreviews; - } - - protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { const local = await this.getSnippetsFileContents(); const localSnippets = this.toSnippetsContents(local); const remoteSnippets: IStringDictionary | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null; @@ -96,102 +75,72 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD return this.getResourcePreviews(mergeResult, local, remoteSnippets || {}); } - protected async updateResourcePreview(resourcePreview: IFileResourcePreview, resource: URI, acceptedContent: string): Promise { - return { - ...resourcePreview, - acceptedContent: acceptedContent || null, - localChange: this.computeLocalChange(resourcePreview, resource, acceptedContent || null), - remoteChange: this.computeRemoteChange(resourcePreview, resource, acceptedContent || null), - }; + protected async getMergeResult(resourcePreview: ISnippetsResourcePreview, token: CancellationToken): Promise { + return resourcePreview.previewResult; } - private computeLocalChange(resourcePreview: IFileResourcePreview, resource: URI, acceptedContent: string | null): Change { - const isRemoteResourceAccepted = isEqualOrParent(resource, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' })); - const isPreviewResourceAccepted = isEqualOrParent(resource, this.syncPreviewFolder); + protected async getAcceptResult(resourcePreview: ISnippetsResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise { - const previewExists = acceptedContent !== null; - const remoteExists = resourcePreview.remoteContent !== null; - const localExists = resourcePreview.fileContent !== null; - - if (isRemoteResourceAccepted) { - if (remoteExists && localExists) { - return Change.Modified; - } - if (remoteExists && !localExists) { - return Change.Added; - } - if (!remoteExists && localExists) { - return Change.Deleted; - } - return Change.None; + /* Accept local resource */ + if (isEqualOrParent(resource, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }))) { + return { + content: resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : null, + localChange: Change.None, + remoteChange: resourcePreview.fileContent + ? resourcePreview.remoteContent !== null ? Change.Modified : Change.Added + : Change.Deleted + }; } - if (isPreviewResourceAccepted) { - if (previewExists && localExists) { - return Change.Modified; - } - if (previewExists && !localExists) { - return Change.Added; - } - if (!previewExists && localExists) { - return Change.Deleted; - } - return Change.None; + /* Accept remote resource */ + if (isEqualOrParent(resource, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }))) { + return { + content: resourcePreview.remoteContent, + localChange: resourcePreview.remoteContent !== null + ? resourcePreview.fileContent ? Change.Modified : Change.Added + : Change.Deleted, + remoteChange: Change.None, + }; } - return Change.None; + /* Accept preview resource */ + if (isEqualOrParent(resource, this.syncPreviewFolder)) { + if (content === undefined) { + return { + content: resourcePreview.previewResult.content, + localChange: resourcePreview.previewResult.localChange, + remoteChange: resourcePreview.previewResult.remoteChange, + }; + } else { + return { + content, + localChange: content === null + ? resourcePreview.fileContent !== null ? Change.Deleted : Change.None + : Change.Modified, + remoteChange: content === null + ? resourcePreview.remoteContent !== null ? Change.Deleted : Change.None + : Change.Modified + }; + } + } + + throw new Error(`Invalid Resource: ${resource.toString()}`); } - private computeRemoteChange(resourcePreview: IFileResourcePreview, resource: URI, acceptedContent: string | null): Change { - const isLocalResourceAccepted = isEqualOrParent(resource, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' })); - const isPreviewResourceAccepted = isEqualOrParent(resource, this.syncPreviewFolder); - - const previewExists = acceptedContent !== null; - const remoteExists = resourcePreview.remoteContent !== null; - const localExists = resourcePreview.fileContent !== null; - - if (isLocalResourceAccepted) { - if (remoteExists && localExists) { - return Change.Modified; - } - if (remoteExists && !localExists) { - return Change.Deleted; - } - if (!remoteExists && localExists) { - return Change.Added; - } - return Change.None; - } - - if (isPreviewResourceAccepted) { - if (previewExists && remoteExists) { - return Change.Modified; - } - if (previewExists && !remoteExists) { - return Change.Added; - } - if (!previewExists && remoteExists) { - return Change.Deleted; - } - return Change.None; - } - - return Change.None; - } - - protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], force: boolean): Promise { - if (resourcePreviews.every(({ localChange, remoteChange }) => localChange === Change.None && remoteChange === Change.None)) { + protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [ISnippetsResourcePreview, IAcceptResult][], force: boolean): Promise { + const accptedResourcePreviews: ISnippetsAcceptedResourcePreview[] = resourcePreviews.map(([resourcePreview, acceptResult]) => ({ ...resourcePreview, acceptResult })); + if (accptedResourcePreviews.every(({ localChange, remoteChange }) => localChange === Change.None && remoteChange === Change.None)) { this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing snippets.`); } - if (resourcePreviews.some(({ localChange }) => localChange !== Change.None)) { + if (accptedResourcePreviews.some(({ localChange }) => localChange !== Change.None)) { // back up all snippets - await this.updateLocalBackup(resourcePreviews); - await this.updateLocalSnippets(resourcePreviews, force); + await this.updateLocalBackup(accptedResourcePreviews); + await this.updateLocalSnippets(accptedResourcePreviews, force); } - if (resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None)) { - remoteUserData = await this.updateRemoteSnippets(resourcePreviews, remoteUserData, force); + if (accptedResourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None)) { + remoteUserData = await this.updateRemoteSnippets(accptedResourcePreviews, remoteUserData, force); } if (lastSyncUserData?.ref !== remoteUserData.ref) { @@ -201,7 +150,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized snippets`); } - for (const { previewResource } of resourcePreviews) { + for (const { previewResource } of accptedResourcePreviews) { // Delete the preview try { await this.fileService.del(previewResource); @@ -210,29 +159,39 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD } - private getResourcePreviews(mergeResult: IMergeResult, localFileContent: IStringDictionary, remoteSnippets: IStringDictionary): IFileResourcePreview[] { - const resourcePreviews: Map = new Map(); + private getResourcePreviews(snippetsMergeResult: ISnippetsMergeResult, localFileContent: IStringDictionary, remoteSnippets: IStringDictionary): ISnippetsResourcePreview[] { + const resourcePreviews: Map = new Map(); /* Snippets added remotely -> add locally */ - for (const key of Object.keys(mergeResult.local.added)) { + for (const key of Object.keys(snippetsMergeResult.local.added)) { + const previewResult: IMergeResult = { + content: snippetsMergeResult.local.added[key], + hasConflicts: false, + localChange: Change.Added, + remoteChange: Change.None, + }; resourcePreviews.set(key, { - localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }), fileContent: null, + localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }), localContent: null, remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }), remoteContent: remoteSnippets[key], previewResource: joinPath(this.syncPreviewFolder, key), - previewContent: mergeResult.local.added[key], - acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }), - acceptedContent: mergeResult.local.added[key], - hasConflicts: false, - localChange: Change.Added, - remoteChange: Change.None + previewResult, + localChange: previewResult.localChange, + remoteChange: previewResult.remoteChange, + acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }) }); } /* Snippets updated remotely -> update locally */ - for (const key of Object.keys(mergeResult.local.updated)) { + for (const key of Object.keys(snippetsMergeResult.local.updated)) { + const previewResult: IMergeResult = { + content: snippetsMergeResult.local.updated[key], + hasConflicts: false, + localChange: Change.Modified, + remoteChange: Change.None, + }; resourcePreviews.set(key, { localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }), fileContent: localFileContent[key], @@ -240,17 +199,21 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }), remoteContent: remoteSnippets[key], previewResource: joinPath(this.syncPreviewFolder, key), - previewContent: mergeResult.local.updated[key], - acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }), - acceptedContent: mergeResult.local.updated[key], - hasConflicts: false, - localChange: Change.Modified, - remoteChange: Change.None + previewResult, + localChange: previewResult.localChange, + remoteChange: previewResult.remoteChange, + acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }) }); } /* Snippets removed remotely -> remove locally */ - for (const key of mergeResult.local.removed) { + for (const key of snippetsMergeResult.local.removed) { + const previewResult: IMergeResult = { + content: null, + hasConflicts: false, + localChange: Change.Deleted, + remoteChange: Change.None, + }; resourcePreviews.set(key, { localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }), fileContent: localFileContent[key], @@ -258,17 +221,21 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }), remoteContent: null, previewResource: joinPath(this.syncPreviewFolder, key), - previewContent: null, - acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }), - acceptedContent: null, - hasConflicts: false, - localChange: Change.Deleted, - remoteChange: Change.None + previewResult, + localChange: previewResult.localChange, + remoteChange: previewResult.remoteChange, + acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }) }); } /* Snippets added locally -> add remotely */ - for (const key of Object.keys(mergeResult.remote.added)) { + for (const key of Object.keys(snippetsMergeResult.remote.added)) { + const previewResult: IMergeResult = { + content: snippetsMergeResult.remote.added[key], + hasConflicts: false, + localChange: Change.None, + remoteChange: Change.Added, + }; resourcePreviews.set(key, { localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }), fileContent: localFileContent[key], @@ -276,17 +243,21 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }), remoteContent: null, previewResource: joinPath(this.syncPreviewFolder, key), - previewContent: mergeResult.remote.added[key], - acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }), - acceptedContent: mergeResult.remote.added[key], - hasConflicts: false, - localChange: Change.None, - remoteChange: Change.Added + previewResult, + localChange: previewResult.localChange, + remoteChange: previewResult.remoteChange, + acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }) }); } /* Snippets updated locally -> update remotely */ - for (const key of Object.keys(mergeResult.remote.updated)) { + for (const key of Object.keys(snippetsMergeResult.remote.updated)) { + const previewResult: IMergeResult = { + content: snippetsMergeResult.remote.updated[key], + hasConflicts: false, + localChange: Change.None, + remoteChange: Change.Modified, + }; resourcePreviews.set(key, { localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }), fileContent: localFileContent[key], @@ -294,17 +265,21 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }), remoteContent: remoteSnippets[key], previewResource: joinPath(this.syncPreviewFolder, key), - previewContent: mergeResult.remote.updated[key], - acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }), - acceptedContent: mergeResult.remote.updated[key], - hasConflicts: false, - localChange: Change.None, - remoteChange: Change.Modified + previewResult, + localChange: previewResult.localChange, + remoteChange: previewResult.remoteChange, + acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }) }); } /* Snippets removed locally -> remove remotely */ - for (const key of mergeResult.remote.removed) { + for (const key of snippetsMergeResult.remote.removed) { + const previewResult: IMergeResult = { + content: null, + hasConflicts: false, + localChange: Change.None, + remoteChange: Change.Deleted, + }; resourcePreviews.set(key, { localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }), fileContent: null, @@ -312,17 +287,21 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }), remoteContent: remoteSnippets[key], previewResource: joinPath(this.syncPreviewFolder, key), - previewContent: null, - acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }), - acceptedContent: null, - hasConflicts: false, - localChange: Change.None, - remoteChange: Change.Deleted + previewResult, + localChange: previewResult.localChange, + remoteChange: previewResult.remoteChange, + acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }) }); } /* Snippets with conflicts */ - for (const key of mergeResult.conflicts) { + for (const key of snippetsMergeResult.conflicts) { + const previewResult: IMergeResult = { + content: localFileContent[key] ? localFileContent[key].value.toString() : null, + hasConflicts: true, + localChange: localFileContent[key] ? Change.Modified : Change.Added, + remoteChange: remoteSnippets[key] ? Change.Modified : Change.Added + }; resourcePreviews.set(key, { localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }), fileContent: localFileContent[key] || null, @@ -330,18 +309,22 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }), remoteContent: remoteSnippets[key] || null, previewResource: joinPath(this.syncPreviewFolder, key), - previewContent: localFileContent[key] ? localFileContent[key].value.toString() : null, - acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }), - acceptedContent: localFileContent[key] ? localFileContent[key].value.toString() : null, - hasConflicts: true, - localChange: localFileContent[key] ? Change.Modified : Change.Added, - remoteChange: remoteSnippets[key] ? Change.Modified : Change.Added + previewResult, + localChange: previewResult.localChange, + remoteChange: previewResult.remoteChange, + acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }) }); } /* Unmodified Snippets */ for (const key of Object.keys(localFileContent)) { if (!resourcePreviews.has(key)) { + const previewResult: IMergeResult = { + content: localFileContent[key] ? localFileContent[key].value.toString() : null, + hasConflicts: false, + localChange: Change.None, + remoteChange: Change.None + }; resourcePreviews.set(key, { localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }), fileContent: localFileContent[key] || null, @@ -349,12 +332,10 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }), remoteContent: remoteSnippets[key] || null, previewResource: joinPath(this.syncPreviewFolder, key), - previewContent: localFileContent[key] ? localFileContent[key].value.toString() : null, - acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }), - acceptedContent: localFileContent[key] ? localFileContent[key].value.toString() : null, - hasConflicts: false, - localChange: Change.None, - remoteChange: Change.None + previewResult, + localChange: previewResult.localChange, + remoteChange: previewResult.remoteChange, + acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }) }); } } @@ -362,7 +343,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD return [...resourcePreviews.values()]; } - async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { + async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> { let content = await super.resolveContent(uri); if (content) { const syncData = this.parseSyncData(content); @@ -373,7 +354,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD const resource = joinPath(uri, snippet); const comparableResource = joinPath(this.snippetsFolder, snippet); const exists = await this.fileService.exists(comparableResource); - result.push({ resource, comparableResource: exists ? comparableResource : undefined }); + result.push({ resource, comparableResource: exists ? comparableResource : joinPath(this.syncPreviewFolder, snippet).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }) }); } return result; } @@ -427,8 +408,8 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD await this.backupLocal(JSON.stringify(this.toSnippetsContents(local))); } - private async updateLocalSnippets(resourcePreviews: IFileResourcePreview[], force: boolean): Promise { - for (const { fileContent, acceptedContent: content, localResource, remoteResource, localChange } of resourcePreviews) { + private async updateLocalSnippets(resourcePreviews: ISnippetsAcceptedResourcePreview[], force: boolean): Promise { + for (const { fileContent, acceptResult, localResource, remoteResource, localChange } of resourcePreviews) { if (localChange !== Change.None) { const key = remoteResource ? basename(remoteResource) : basename(localResource!); const resource = joinPath(this.snippetsFolder, key); @@ -443,31 +424,31 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD // Added else if (localChange === Change.Added) { this.logService.trace(`${this.syncResourceLogLabel}: Creating snippet...`, basename(resource)); - await this.fileService.createFile(resource, VSBuffer.fromString(content!), { overwrite: force }); + await this.fileService.createFile(resource, VSBuffer.fromString(acceptResult.content!), { overwrite: force }); this.logService.info(`${this.syncResourceLogLabel}: Created snippet`, basename(resource)); } // Updated else { this.logService.trace(`${this.syncResourceLogLabel}: Updating snippet...`, basename(resource)); - await this.fileService.writeFile(resource, VSBuffer.fromString(content!), force ? undefined : fileContent!); + await this.fileService.writeFile(resource, VSBuffer.fromString(acceptResult.content!), force ? undefined : fileContent!); this.logService.info(`${this.syncResourceLogLabel}: Updated snippet`, basename(resource)); } } } } - private async updateRemoteSnippets(resourcePreviews: IFileResourcePreview[], remoteUserData: IRemoteUserData, forcePush: boolean): Promise { + private async updateRemoteSnippets(resourcePreviews: ISnippetsAcceptedResourcePreview[], remoteUserData: IRemoteUserData, forcePush: boolean): Promise { const currentSnippets: IStringDictionary = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : {}; const newSnippets: IStringDictionary = deepClone(currentSnippets); - for (const { acceptedContent: content, localResource, remoteResource, remoteChange } of resourcePreviews) { + for (const { acceptResult, localResource, remoteResource, remoteChange } of resourcePreviews) { if (remoteChange !== Change.None) { const key = localResource ? basename(localResource) : basename(remoteResource!); if (remoteChange === Change.Deleted) { delete newSnippets[key]; } else { - newSnippets[key] = content!; + newSnippets[key] = acceptResult.content!; } } } diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index 511df050312..097be01f51d 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -6,7 +6,7 @@ import { Delayer, disposableTimeout, CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, toDisposable, MutableDisposable, IDisposable } from 'vs/base/common/lifecycle'; -import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, UserDataAutoSyncError, ISyncTask } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, UserDataAutoSyncError, ISyncTask, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { isPromiseCanceledError } from 'vs/base/common/errors'; @@ -15,6 +15,9 @@ import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/ import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; import { localize } from 'vs/nls'; +import { toLocalISOString } from 'vs/base/common/date'; +import { URI } from 'vs/base/common/uri'; +import { isEqual } from 'vs/base/common/resources'; type AutoSyncClassification = { sources: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -26,11 +29,13 @@ type AutoSyncEnablementClassification = { type AutoSyncErrorClassification = { code: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; + service: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; }; const enablementKey = 'sync.enable'; const disableMachineEventuallyKey = 'sync.disableMachineEventually'; const sessionIdKey = 'sync.sessionId'; +const storeUrlKey = 'sync.storeUrl'; export class UserDataAutoSyncEnablementService extends Disposable { @@ -39,7 +44,8 @@ export class UserDataAutoSyncEnablementService extends Disposable { constructor( @IStorageService protected readonly storageService: IStorageService, - @IEnvironmentService private readonly environmentService: IEnvironmentService + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IUserDataSyncStoreManagementService protected readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService ) { super(); this._register(storageService.onDidChangeStorage(e => this.onDidStorageChange(e))); @@ -56,7 +62,7 @@ export class UserDataAutoSyncEnablementService extends Disposable { } canToggleEnablement(): boolean { - return this.environmentService.sync === undefined; + return this.userDataSyncStoreManagementService.userDataSyncStore !== undefined && this.environmentService.sync === undefined; } private onDidStorageChange(workspaceStorageChangeEvent: IWorkspaceStorageChangeEvent): void { @@ -81,7 +87,21 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i private readonly _onError: Emitter = this._register(new Emitter()); readonly onError: Event = this._onError.event; + private lastSyncUrl: URI | undefined; + private get syncUrl(): URI | undefined { + const value = this.storageService.get(storeUrlKey, StorageScope.GLOBAL); + return value ? URI.parse(value) : undefined; + } + private set syncUrl(syncUrl: URI | undefined) { + if (syncUrl) { + this.storageService.store(storeUrlKey, syncUrl.toString(), StorageScope.GLOBAL); + } else { + this.storageService.remove(storeUrlKey, StorageScope.GLOBAL); + } + } + constructor( + @IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @@ -92,25 +112,35 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i @IStorageService storageService: IStorageService, @IEnvironmentService environmentService: IEnvironmentService ) { - super(storageService, environmentService); + super(storageService, environmentService, userDataSyncStoreManagementService); this.syncTriggerDelayer = this._register(new Delayer(0)); + this.lastSyncUrl = this.syncUrl; + this.syncUrl = userDataSyncStoreManagementService.userDataSyncStore?.url; - if (userDataSyncStoreService.userDataSyncStore) { + if (userDataSyncStoreManagementService.userDataSyncStore) { + if (this.isEnabled()) { + this.logService.info('Auto Sync is enabled.'); + } else { + this.logService.info('Auto Sync is disabled.'); + } this.updateAutoSync(); + if (this.hasToDisableMachineEventually()) { this.disableMachineEventually(); } + 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))); } } private updateAutoSync(): void { - const { enabled, reason } = this.isAutoSyncEnabled(); + const { enabled, message } = this.isAutoSyncEnabled(); if (enabled) { if (this.autoSync.value === undefined) { - this.autoSync.value = new AutoSync(1000 * 60 * 5 /* 5 miutes */, this.userDataSyncStoreService, this.userDataSyncService, this.userDataSyncMachinesService, this.logService, this.storageService); + this.autoSync.value = new AutoSync(this.lastSyncUrl, 1000 * 60 * 5 /* 5 miutes */, this.userDataSyncStoreManagementService, this.userDataSyncStoreService, this.userDataSyncService, this.userDataSyncMachinesService, this.logService, this.storageService); this.autoSync.value.register(this.autoSync.value.onDidStartSync(() => this.lastSyncTriggerTime = new Date().getTime())); this.autoSync.value.register(this.autoSync.value.onDidFinishSync(e => this.onDidFinishSync(e))); if (this.startAutoSync()) { @@ -120,27 +150,38 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i } else { this.syncTriggerDelayer.cancel(); if (this.autoSync.value !== undefined) { - this.logService.info('Auto Sync: Disabled because', reason); + if (message) { + this.logService.info(message); + } this.autoSync.clear(); } + + /* log message when auto sync is not disabled by user */ + else if (message && this.isEnabled()) { + this.logService.info(message); + } } } // For tests purpose only protected startAutoSync(): boolean { return true; } - private isAutoSyncEnabled(): { enabled: boolean, reason?: string } { + private isAutoSyncEnabled(): { enabled: boolean, message?: string } { if (!this.isEnabled()) { - return { enabled: false, reason: 'sync is disabled' }; + return { enabled: false, message: 'Auto Sync: Disabled.' }; } if (!this.userDataSyncAccountService.account) { - return { enabled: false, reason: 'token is not avaialable' }; + return { enabled: false, message: 'Auto Sync: Suspended until auth token is available.' }; + } + if (this.userDataSyncStoreService.donotMakeRequestsUntil) { + return { enabled: false, message: `Auto Sync: Suspended until ${toLocalISOString(this.userDataSyncStoreService.donotMakeRequestsUntil)} because server is not accepting requests until then.` }; } return { enabled: true }; } async turnOn(): Promise { this.stopDisableMachineEventually(); + this.lastSyncUrl = this.syncUrl; this.setEnablement(true); } @@ -195,7 +236,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i // Log to telemetry if (userDataSyncError instanceof UserDataAutoSyncError) { - this.telemetryService.publicLog2<{ code: string }, AutoSyncErrorClassification>(`autosync/error`, { code: userDataSyncError.code }); + this.telemetryService.publicLog2<{ code: string, service: string }, AutoSyncErrorClassification>(`autosync/error`, { code: userDataSyncError.code, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() }); } // Session got expired @@ -205,7 +246,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i } // Turned off from another device - if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff) { + else if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff) { await this.turnOff(false, true /* force soft turnoff on error */); this.logService.info('Auto Sync: Turned off sync because sync is turned off in the cloud'); } @@ -229,13 +270,20 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i // Incompatible Local Content else if (userDataSyncError.code === UserDataSyncErrorCode.IncompatibleLocalContent) { await this.turnOff(false, true /* force soft turnoff on error */); - this.logService.info('Auto Sync: Turned off sync because server has {0} content with newer version than of client. Requires client upgrade.', userDataSyncError.resource); + this.logService.info(`Auto Sync: Turned off sync because server has ${userDataSyncError.resource} content with newer version than of client. Requires client upgrade.`); } // Incompatible Remote Content else if (userDataSyncError.code === UserDataSyncErrorCode.IncompatibleRemoteContent) { await this.turnOff(false, true /* force soft turnoff on error */); - this.logService.info('Auto Sync: Turned off sync because server has {0} content with older version than of client. Requires server reset.', userDataSyncError.resource); + this.logService.info(`Auto Sync: Turned off sync because server has ${userDataSyncError.resource} content with older version than of client. Requires server reset.`); + } + + // Service changed + else if (userDataSyncError.code === UserDataSyncErrorCode.ServiceChanged || userDataSyncError.code === UserDataSyncErrorCode.DefaultServiceChanged) { + await this.turnOff(false, true /* force soft turnoff on error */, true /* do not disable machine */); + await this.turnOn(); + this.logService.info('Auto Sync: Sync Service changed. Turned off auto sync, reset local state and turned on auto sync.'); } else { @@ -319,7 +367,9 @@ class AutoSync extends Disposable { private syncPromise: CancelablePromise | undefined; constructor( + private readonly lastSyncUrl: URI | undefined, private readonly interval: number /* in milliseconds */, + private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, private readonly userDataSyncStoreService: IUserDataSyncStoreService, private readonly userDataSyncService: IUserDataSyncService, private readonly userDataSyncMachinesService: IUserDataSyncMachinesService, @@ -334,7 +384,7 @@ class AutoSync extends Disposable { this._register(toDisposable(() => { if (this.syncPromise) { this.syncPromise.cancel(); - this.logService.info('Auto sync: Canelled sync that is in progress'); + this.logService.info('Auto sync: Cancelled sync that is in progress'); this.syncPromise = undefined; } if (this.syncTask) { @@ -371,6 +421,20 @@ class AutoSync extends Disposable { return this.syncPromise; } + private hasSyncServiceChanged(): boolean { + return this.lastSyncUrl !== undefined && !isEqual(this.lastSyncUrl, this.userDataSyncStoreManagementService.userDataSyncStore?.url); + } + + private async hasDefaultServiceChanged(): Promise { + const previous = await this.userDataSyncStoreManagementService.getPreviousUserDataSyncStore(); + const current = this.userDataSyncStoreManagementService.userDataSyncStore; + // check if defaults changed + return !!current && !!previous && + (!isEqual(current.defaultUrl, previous.defaultUrl) || + !isEqual(current.insidersUrl, previous.insidersUrl) || + !isEqual(current.stableUrl, previous.stableUrl)); + } + private async doSync(reason: string, token: CancellationToken): Promise { this.logService.info(`Auto Sync: Triggered by ${reason}`); this._onDidStartSync.fire(); @@ -384,14 +448,30 @@ class AutoSync extends Disposable { // Server has no data but this machine was synced before if (manifest === null && await this.userDataSyncService.hasPreviouslySynced()) { - // Sync was turned off in the cloud - throw new UserDataAutoSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff); + if (this.hasSyncServiceChanged()) { + if (await this.hasDefaultServiceChanged()) { + throw new UserDataAutoSyncError(localize('default service changed', "Cannot sync because default service has changed"), UserDataSyncErrorCode.DefaultServiceChanged); + } else { + throw new UserDataAutoSyncError(localize('service changed', "Cannot sync because sync service has changed"), UserDataSyncErrorCode.ServiceChanged); + } + } else { + // Sync was turned off in the cloud + throw new UserDataAutoSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff); + } } const sessionId = this.storageService.get(sessionIdKey, StorageScope.GLOBAL); // Server session is different from client session if (sessionId && manifest && sessionId !== manifest.session) { - throw new UserDataAutoSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired); + if (this.hasSyncServiceChanged()) { + if (await this.hasDefaultServiceChanged()) { + throw new UserDataAutoSyncError(localize('default service changed', "Cannot sync because default service has changed"), UserDataSyncErrorCode.DefaultServiceChanged); + } else { + throw new UserDataAutoSyncError(localize('service changed', "Cannot sync because sync service has changed"), UserDataSyncErrorCode.ServiceChanged); + } + } else { + throw new UserDataAutoSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired); + } } const machines = await this.userDataSyncMachinesService.getMachines(manifest || undefined); diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 0cd0adf6049..dbc24ef949e 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -13,13 +13,11 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { ILogService } from 'vs/platform/log/common/log'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { URI } from 'vs/base/common/uri'; import { joinPath, isEqualOrParent } from 'vs/base/common/resources'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IProductService, ConfigurationSyncStore } from 'vs/platform/product/common/productService'; import { distinct } from 'vs/base/common/arrays'; import { isArray, isString, isObject } from 'vs/base/common/types'; import { IHeaders } from 'vs/base/parts/request/common/request'; @@ -43,21 +41,25 @@ export function registerConfiguration(): IDisposable { const ignoredExtensionsSchemaId = 'vscode://schemas/ignoredExtensions'; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ - id: 'sync', + id: 'settingsSync', order: 30, - title: localize('sync', "Sync"), + title: localize('settings sync', "Settings Sync"), type: 'object', properties: { - 'sync.keybindingsPerPlatform': { + 'settingsSync.keybindingsPerPlatform': { type: 'boolean', - description: localize('sync.keybindingsPerPlatform', "Synchronize keybindings per platform."), + description: localize('settingsSync.keybindingsPerPlatform', "Synchronize keybindings for each platform."), default: true, scope: ConfigurationScope.APPLICATION, tags: ['sync', 'usesOnlineServices'] }, - 'sync.ignoredExtensions': { + 'sync.keybindingsPerPlatform': { + type: 'boolean', + deprecationMessage: localize('sync.keybindingsPerPlatform.deprecated', "Deprecated, use settingsSync.keybindingsPerPlatform instead"), + }, + 'settingsSync.ignoredExtensions': { 'type': 'array', - 'description': localize('sync.ignoredExtensions', "List of extensions to be ignored while synchronizing. The identifier of an extension is always ${publisher}.${name}. For example: vscode.csharp."), + markdownDescription: localize('settingsSync.ignoredExtensions', "List of extensions to be ignored while synchronizing. The identifier of an extension is always `${publisher}.${name}`. For example: `vscode.csharp`."), $ref: ignoredExtensionsSchemaId, 'default': [], 'scope': ConfigurationScope.APPLICATION, @@ -65,9 +67,13 @@ export function registerConfiguration(): IDisposable { disallowSyncIgnore: true, tags: ['sync', 'usesOnlineServices'] }, - 'sync.ignoredSettings': { + 'sync.ignoredExtensions': { 'type': 'array', - description: localize('sync.ignoredSettings', "Configure settings to be ignored while synchronizing."), + deprecationMessage: localize('sync.ignoredExtensions.deprecated', "Deprecated, use settingsSync.ignoredExtensions instead"), + }, + 'settingsSync.ignoredSettings': { + 'type': 'array', + description: localize('settingsSync.ignoredSettings', "Configure settings to be ignored while synchronizing."), 'default': [], 'scope': ConfigurationScope.APPLICATION, $ref: ignoredSettingsSchemaId, @@ -75,6 +81,10 @@ export function registerConfiguration(): IDisposable { uniqueItems: true, disallowSyncIgnore: true, tags: ['sync', 'usesOnlineServices'] + }, + 'sync.ignoredSettings': { + 'type': 'array', + deprecationMessage: localize('sync.ignoredSettings.deprecated', "Deprecated, use settingsSync.ignoredSettings instead"), } } }); @@ -110,8 +120,11 @@ export interface IUserData { export type IAuthenticationProvider = { id: string, scopes: string[] }; export interface IUserDataSyncStore { - url: URI; - authenticationProviders: IAuthenticationProvider[]; + readonly url: URI; + readonly defaultUrl: URI; + readonly stableUrl: URI | undefined; + readonly insidersUrl: URI | undefined; + readonly authenticationProviders: IAuthenticationProvider[]; } export function isAuthenticationProvider(thing: any): thing is IAuthenticationProvider { @@ -121,24 +134,6 @@ export function isAuthenticationProvider(thing: any): thing is IAuthenticationPr && isArray(thing.scopes); } -export function getUserDataSyncStore(productService: IProductService, configurationService: IConfigurationService): IUserDataSyncStore | undefined { - const value = configurationService.getValue(CONFIGURATION_SYNC_STORE_KEY) || productService[CONFIGURATION_SYNC_STORE_KEY]; - if (value - && isString(value.url) - && isObject(value.authenticationProviders) - && Object.keys(value.authenticationProviders).every(authenticationProviderId => isArray(value.authenticationProviders[authenticationProviderId].scopes)) - ) { - return { - url: joinPath(URI.parse(value.url), 'v1'), - authenticationProviders: Object.keys(value.authenticationProviders).reduce((result, id) => { - result.push({ id, scopes: value.authenticationProviders[id].scopes }); - return result; - }, []) - }; - } - return undefined; -} - export const enum SyncResource { Settings = 'settings', Keybindings = 'keybindings', @@ -158,11 +153,23 @@ export interface IResourceRefHandle { created: number; } -export const IUserDataSyncStoreService = createDecorator('IUserDataSyncStoreService'); export type ServerResource = SyncResource | 'machines'; -export interface IUserDataSyncStoreService { +export type UserDataSyncStoreType = 'insiders' | 'stable'; + +export const IUserDataSyncStoreManagementService = createDecorator('IUserDataSyncStoreManagementService'); +export interface IUserDataSyncStoreManagementService { readonly _serviceBrand: undefined; readonly userDataSyncStore: IUserDataSyncStore | undefined; + switch(type: UserDataSyncStoreType): Promise; + getPreviousUserDataSyncStore(): Promise; +} + +export const IUserDataSyncStoreService = createDecorator('IUserDataSyncStoreService'); +export interface IUserDataSyncStoreService { + readonly _serviceBrand: undefined; + + readonly onDidChangeDonotMakeRequestsUntil: Event; + readonly donotMakeRequestsUntil: Date | undefined; readonly onTokenFailed: Event; readonly onTokenSucceed: Event; @@ -207,12 +214,15 @@ export enum UserDataSyncErrorCode { UpgradeRequired = 'UpgradeRequired', /* 426 */ PreconditionRequired = 'PreconditionRequired', /* 428 */ TooManyRequests = 'RemoteTooManyRequests', /* 429 */ + TooManyRequestsAndRetryAfter = 'TooManyRequestsAndRetryAfter', /* 429 + Retry-After */ // Local Errors ConnectionRefused = 'ConnectionRefused', NoRef = 'NoRef', TurnedOff = 'TurnedOff', SessionExpired = 'SessionExpired', + ServiceChanged = 'ServiceChanged', + DefaultServiceChanged = 'DefaultServiceChanged', LocalTooManyRequests = 'LocalTooManyRequests', LocalPreconditionFailed = 'LocalPreconditionFailed', LocalInvalidContent = 'LocalInvalidContent', @@ -349,14 +359,12 @@ export interface IUserDataSynchroniser { readonly onDidChangeLocal: Event; - pull(): Promise; - push(): Promise; sync(manifest: IUserDataManifest | null, headers: IHeaders): Promise; replace(uri: URI): Promise; stop(): Promise; preview(manifest: IUserDataManifest | null, headers: IHeaders): Promise; - accept(resource: URI, content: string): Promise; + accept(resource: URI, content?: string | null): Promise; merge(resource: URI): Promise; discard(resource: URI): Promise; apply(force: boolean, headers: IHeaders): Promise; @@ -368,7 +376,7 @@ export interface IUserDataSynchroniser { resolveContent(resource: URI): Promise; getRemoteSyncResourceHandles(): Promise; getLocalSyncResourceHandles(): Promise; - getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]>; + getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]>; getMachineId(syncResourceHandle: ISyncResourceHandle): Promise; } @@ -393,12 +401,14 @@ export interface ISyncTask { export interface IManualSyncTask extends IDisposable { readonly id: string; + readonly status: SyncStatus; readonly manifest: IUserDataManifest | null; readonly onSynchronizeResources: Event<[SyncResource, URI[]][]>; preview(): Promise<[SyncResource, ISyncResourcePreview][]>; - accept(resource: URI, content: string): Promise<[SyncResource, ISyncResourcePreview][]>; - merge(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]>; + accept(resource: URI, content?: string | null): Promise<[SyncResource, ISyncResourcePreview][]>; + merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]>; discard(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]>; + discardConflicts(): Promise<[SyncResource, ISyncResourcePreview][]>; apply(): Promise<[SyncResource, ISyncResourcePreview][]>; pull(): Promise; push(): Promise; @@ -421,10 +431,12 @@ export interface IUserDataSyncService { readonly lastSyncTime: number | undefined; readonly onDidChangeLastSyncTime: Event; + readonly onDidResetRemote: Event; + readonly onDidResetLocal: Event; + createSyncTask(): Promise; createManualSyncTask(): Promise; - pull(): Promise; replace(uri: URI): Promise; reset(): Promise; resetRemote(): Promise; @@ -433,11 +445,11 @@ export interface IUserDataSyncService { hasLocalData(): Promise; hasPreviouslySynced(): Promise; resolveContent(resource: URI): Promise; - accept(resource: SyncResource, conflictResource: URI, content: string, apply: boolean): Promise; + accept(resource: SyncResource, conflictResource: URI, content: string | null | undefined, apply: boolean): Promise; getLocalSyncResourceHandles(resource: SyncResource): Promise; getRemoteSyncResourceHandles(resource: SyncResource): Promise; - getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]>; + getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]>; getMachineId(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise; } diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 97789537313..8d6296e8051 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -5,7 +5,7 @@ import { IServerChannel, IChannel, IPCServer } from 'vs/base/parts/ipc/common/ipc'; import { Event, Emitter } from 'vs/base/common/event'; -import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService, IManualSyncTask, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService, IManualSyncTask, IUserDataManifest, IUserDataSyncStoreManagementService, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync'; import { URI } from 'vs/base/common/uri'; import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; @@ -26,6 +26,8 @@ export class UserDataSyncChannel implements IServerChannel { case 'onDidChangeLocal': return this.service.onDidChangeLocal; case 'onDidChangeLastSyncTime': return this.service.onDidChangeLastSyncTime; case 'onSyncErrors': return this.service.onSyncErrors; + case 'onDidResetLocal': return this.service.onDidResetLocal; + case 'onDidResetRemote': return this.service.onDidResetRemote; } throw new Error(`Event not found: ${event}`); } @@ -46,7 +48,6 @@ export class UserDataSyncChannel implements IServerChannel { case 'createManualSyncTask': return this.createManualSyncTask(); - case 'pull': return this.service.pull(); case 'replace': return this.service.replace(URI.revive(args[0])); case 'reset': return this.service.reset(); case 'resetRemote': return this.service.resetRemote(); @@ -63,17 +64,20 @@ export class UserDataSyncChannel implements IServerChannel { throw new Error('Invalid call'); } - private async createManualSyncTask(): Promise<{ id: string, manifest: IUserDataManifest | null }> { + private async createManualSyncTask(): Promise<{ id: string, manifest: IUserDataManifest | null, status: SyncStatus }> { const manualSyncTask = await this.service.createManualSyncTask(); - const manualSyncTaskChannel = new ManualSyncTaskChannel(manualSyncTask); + const manualSyncTaskChannel = new ManualSyncTaskChannel(manualSyncTask, this.logService); this.server.registerChannel(`manualSyncTask-${manualSyncTask.id}`, manualSyncTaskChannel); - return { id: manualSyncTask.id, manifest: manualSyncTask.manifest }; + return { id: manualSyncTask.id, manifest: manualSyncTask.manifest, status: manualSyncTask.status }; } } class ManualSyncTaskChannel implements IServerChannel { - constructor(private readonly manualSyncTask: IManualSyncTask) { } + constructor( + private readonly manualSyncTask: IManualSyncTask, + private readonly logService: ILogService + ) { } listen(_: unknown, event: string): Event { switch (event) { @@ -83,15 +87,27 @@ class ManualSyncTaskChannel implements IServerChannel { } async call(context: any, command: string, args?: any): Promise { + try { + const result = await this._call(context, command, args); + return result; + } catch (e) { + this.logService.error(e); + throw e; + } + } + + private async _call(context: any, command: string, args?: any): Promise { switch (command) { case 'preview': return this.manualSyncTask.preview(); case 'accept': return this.manualSyncTask.accept(URI.revive(args[0]), args[1]); case 'merge': return this.manualSyncTask.merge(URI.revive(args[0])); case 'discard': return this.manualSyncTask.discard(URI.revive(args[0])); + case 'discardConflicts': return this.manualSyncTask.discardConflicts(); case 'apply': return this.manualSyncTask.apply(); case 'pull': return this.manualSyncTask.pull(); case 'push': return this.manualSyncTask.push(); case 'stop': return this.manualSyncTask.stop(); + case '_getStatus': return this.manualSyncTask.status; case 'dispose': return this.manualSyncTask.dispose(); } throw new Error('Invalid call'); @@ -212,6 +228,9 @@ export class UserDataSyncMachinesServiceChannel implements IServerChannel { constructor(private readonly service: IUserDataSyncMachinesService) { } listen(_: unknown, event: string): Event { + switch (event) { + case 'onDidChange': return this.service.onDidChange; + } throw new Error(`Event not found: ${event}`); } @@ -248,3 +267,18 @@ export class UserDataSyncAccountServiceChannel implements IServerChannel { } } +export class UserDataSyncStoreManagementServiceChannel implements IServerChannel { + constructor(private readonly service: IUserDataSyncStoreManagementService) { } + + listen(_: unknown, event: string): Event { + throw new Error(`Event not found: ${event}`); + } + + call(context: any, command: string, args?: any): Promise { + switch (command) { + case 'switch': return this.service.switch(args[0]); + case 'getPreviousUserDataSyncStore': return this.service.getPreviousUserDataSyncStore(); + } + throw new Error('Invalid call'); + } +} diff --git a/src/vs/platform/userDataSync/common/userDataSyncMachines.ts b/src/vs/platform/userDataSync/common/userDataSyncMachines.ts index 0bd7cd93992..1b9d608b744 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncMachines.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncMachines.ts @@ -14,6 +14,7 @@ import { localize } from 'vs/nls'; import { IProductService } from 'vs/platform/product/common/productService'; import { PlatformToString, isWeb, Platform, platform } from 'vs/base/common/platform'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; +import { Event, Emitter } from 'vs/base/common/event'; interface IMachineData { id: string; @@ -32,6 +33,8 @@ export const IUserDataSyncMachinesService = createDecorator; + getMachines(manifest?: IUserDataManifest): Promise; addCurrentMachine(manifest?: IUserDataManifest): Promise; @@ -49,6 +52,9 @@ export class UserDataSyncMachinesService extends Disposable implements IUserData _serviceBrand: any; + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + private readonly currentMachineIdPromise: Promise; private userData: IUserData | null = null; @@ -118,7 +124,7 @@ export class UserDataSyncMachinesService extends Disposable implements IUserData } const namePrefix = `${this.productService.nameLong} (${PlatformToString(isWeb ? Platform.Web : platform)})`; - const nameRegEx = new RegExp(`${escapeRegExpCharacters(namePrefix)}\\s#(\\d)`); + const nameRegEx = new RegExp(`${escapeRegExpCharacters(namePrefix)}\\s#(\\d+)`); let nameIndex = 0; for (const machine of machines) { const matches = nameRegEx.exec(machine.name); @@ -141,6 +147,7 @@ export class UserDataSyncMachinesService extends Disposable implements IUserData const content = JSON.stringify(machinesData); const ref = await this.userDataSyncStoreService.write(UserDataSyncMachinesService.RESOURCE, content, this.userData?.ref || null); this.userData = { ref, content }; + this._onDidChange.fire(); } private async readUserData(manifest?: IUserDataManifest): Promise { diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 7e3485683cc..f9c446c2124 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -5,7 +5,7 @@ import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, - UserDataSyncError, ISyncResourceHandle, IUserDataManifest, ISyncTask, IResourcePreview, IManualSyncTask, ISyncResourcePreview, HEADER_EXECUTION_ID + UserDataSyncError, ISyncResourceHandle, IUserDataManifest, ISyncTask, IResourcePreview, IManualSyncTask, ISyncResourcePreview, HEADER_EXECUTION_ID, MergeState, Change, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; import { Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -28,6 +28,8 @@ import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async import { isPromiseCanceledError } from 'vs/base/common/errors'; type SyncErrorClassification = { + code: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; + service: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; resource?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; executionId?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; }; @@ -67,6 +69,11 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ private _onDidChangeLastSyncTime: Emitter = this._register(new Emitter()); readonly onDidChangeLastSyncTime: Event = this._onDidChangeLastSyncTime.event; + private _onDidResetLocal = this._register(new Emitter()); + readonly onDidResetLocal = this._onDidResetLocal.event; + private _onDidResetRemote = this._register(new Emitter()); + readonly onDidResetRemote = this._onDidResetRemote.event; + private readonly settingsSynchroniser: SettingsSynchroniser; private readonly keybindingsSynchroniser: KeybindingsSynchroniser; private readonly snippetsSynchroniser: SnippetsSynchroniser; @@ -75,6 +82,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ constructor( @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -89,7 +97,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.globalStateSynchroniser, this.extensionsSynchroniser]; this.updateStatus(); - if (this.userDataSyncStoreService.userDataSyncStore) { + if (this.userDataSyncStoreManagementService.userDataSyncStore) { this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus())); this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeConflicts, () => undefined)))(() => this.updateConflicts())); } @@ -98,44 +106,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeLocal, () => s.resource))); } - async pull(): Promise { - await this.checkEnablement(); - try { - for (const synchroniser of this.synchronisers) { - try { - await synchroniser.pull(); - } catch (e) { - this.handleSynchronizerError(e, synchroniser.resource); - } - } - this.updateLastSyncTime(); - } catch (error) { - if (error instanceof UserDataSyncError) { - this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource }); - } - throw error; - } - } - - async push(): Promise { - await this.checkEnablement(); - try { - for (const synchroniser of this.synchronisers) { - try { - await synchroniser.push(); - } catch (e) { - this.handleSynchronizerError(e, synchroniser.resource); - } - } - this.updateLastSyncTime(); - } catch (error) { - if (error instanceof UserDataSyncError) { - this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource }); - } - throw error; - } - } - async createSyncTask(): Promise { await this.checkEnablement(); @@ -144,9 +114,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ try { manifest = await this.userDataSyncStoreService.manifest(createSyncHeaders(executionId)); } catch (error) { - if (error instanceof UserDataSyncError) { - this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource, executionId }); - } + 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() }); throw error; } @@ -181,9 +150,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ try { manifest = await this.userDataSyncStoreService.manifest(syncHeaders); } catch (error) { - if (error instanceof UserDataSyncError) { - this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource, executionId }); - } + 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() }); throw error; } @@ -228,9 +196,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`); this.updateLastSyncTime(); } catch (error) { - if (error instanceof UserDataSyncError) { - this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource, executionId }); - } + 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() }); throw error; } finally { this.updateStatus(); @@ -264,7 +231,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } } - async accept(syncResource: SyncResource, resource: URI, content: string, apply: boolean): Promise { + async accept(syncResource: SyncResource, resource: URI, content: string | null | undefined, apply: boolean): Promise { await this.checkEnablement(); const synchroniser = this.getSynchroniser(syncResource); await synchroniser.accept(resource, content); @@ -291,7 +258,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return this.getSynchroniser(resource).getLocalSyncResourceHandles(); } - getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { + getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> { return this.getSynchroniser(resource).getAssociatedResources(syncResourceHandle); } @@ -324,6 +291,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } catch (e) { this.logService.error(e); } + this._onDidResetRemote.fire(); } async resetLocal(): Promise { @@ -337,6 +305,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.logService.error(e); } } + this._onDidResetLocal.fire(); this.logService.info('Did reset the local sync state.'); } @@ -375,7 +344,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } private computeStatus(): SyncStatus { - if (!this.userDataSyncStoreService.userDataSyncStore) { + if (!this.userDataSyncStoreManagementService.userDataSyncStore) { return SyncStatus.Uninitialized; } if (this.synchronisers.some(s => s.status === SyncStatus.HasConflicts)) { @@ -402,6 +371,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ throw new UserDataSyncError(e.message, e.code, source); case UserDataSyncErrorCode.TooManyRequests: + case UserDataSyncErrorCode.TooManyRequestsAndRetryAfter: case UserDataSyncErrorCode.LocalTooManyRequests: case UserDataSyncErrorCode.Gone: case UserDataSyncErrorCode.UpgradeRequired: @@ -424,7 +394,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } private async checkEnablement(): Promise { - if (!this.userDataSyncStoreService.userDataSyncStore) { + if (!this.userDataSyncStoreManagementService.userDataSyncStore) { throw new Error('Not enabled'); } } @@ -442,6 +412,16 @@ class ManualSyncTask extends Disposable implements IManualSyncTask { private isDisposed: boolean = false; + get status(): SyncStatus { + if (this.synchronisers.some(s => s.status === SyncStatus.HasConflicts)) { + return SyncStatus.HasConflicts; + } + if (this.synchronisers.some(s => s.status === SyncStatus.Syncing)) { + return SyncStatus.Syncing; + } + return SyncStatus.Idle; + } + constructor( readonly id: string, readonly manifest: IUserDataManifest | null, @@ -459,22 +439,140 @@ class ManualSyncTask extends Disposable implements IManualSyncTask { if (!this.previewsPromise) { this.previewsPromise = createCancelablePromise(token => this.getPreviews(token)); } - this.previews = await this.previewsPromise; + if (!this.previews) { + this.previews = await this.previewsPromise; + } return this.previews; } - async accept(resource: URI, content: string): Promise<[SyncResource, ISyncResourcePreview][]> { + async accept(resource: URI, content?: string | null): Promise<[SyncResource, ISyncResourcePreview][]> { return this.performAction(resource, sychronizer => sychronizer.accept(resource, content)); } - async merge(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]> { - return this.performAction(resource, sychronizer => sychronizer.merge(resource)); + async merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]> { + if (resource) { + return this.performAction(resource, sychronizer => sychronizer.merge(resource)); + } else { + return this.mergeAll(); + } } async discard(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]> { return this.performAction(resource, sychronizer => sychronizer.discard(resource)); } + async discardConflicts(): Promise<[SyncResource, ISyncResourcePreview][]> { + if (!this.previews) { + throw new Error('Missing preview. Create preview and try again.'); + } + if (this.synchronizingResources.length) { + throw new Error('Cannot discard while synchronizing resources'); + } + + const conflictResources: URI[] = []; + for (const [, syncResourcePreview] of this.previews) { + for (const resourcePreview of syncResourcePreview.resourcePreviews) { + if (resourcePreview.mergeState === MergeState.Conflict) { + conflictResources.push(resourcePreview.previewResource); + } + } + } + + for (const resource of conflictResources) { + await this.discard(resource); + } + return this.previews; + } + + async apply(): Promise<[SyncResource, ISyncResourcePreview][]> { + if (!this.previews) { + throw new Error('You need to create preview before applying'); + } + if (this.synchronizingResources.length) { + throw new Error('Cannot pull while synchronizing resources'); + } + const previews: [SyncResource, ISyncResourcePreview][] = []; + for (const [syncResource, preview] of this.previews) { + this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); + this._onSynchronizeResources.fire(this.synchronizingResources); + + const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; + + /* merge those which are not yet merged */ + for (const resourcePreview of preview.resourcePreviews) { + if ((resourcePreview.localChange !== Change.None || resourcePreview.remoteChange !== Change.None) && resourcePreview.mergeState === MergeState.Preview) { + await synchroniser.merge(resourcePreview.previewResource); + } + } + + /* apply */ + const newPreview = await synchroniser.apply(false, this.syncHeaders); + if (newPreview) { + previews.push(this.toSyncResourcePreview(synchroniser.resource, newPreview)); + } + + this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); + this._onSynchronizeResources.fire(this.synchronizingResources); + } + this.previews = previews; + return this.previews; + } + + async pull(): Promise { + if (!this.previews) { + throw new Error('You need to create preview before applying'); + } + if (this.synchronizingResources.length) { + throw new Error('Cannot pull while synchronizing resources'); + } + for (const [syncResource, preview] of this.previews) { + this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); + this._onSynchronizeResources.fire(this.synchronizingResources); + const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; + for (const resourcePreview of preview.resourcePreviews) { + await synchroniser.accept(resourcePreview.remoteResource); + } + await synchroniser.apply(true, this.syncHeaders); + this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); + this._onSynchronizeResources.fire(this.synchronizingResources); + } + this.previews = []; + } + + async push(): Promise { + if (!this.previews) { + throw new Error('You need to create preview before applying'); + } + if (this.synchronizingResources.length) { + throw new Error('Cannot pull while synchronizing resources'); + } + for (const [syncResource, preview] of this.previews) { + this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); + this._onSynchronizeResources.fire(this.synchronizingResources); + const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; + for (const resourcePreview of preview.resourcePreviews) { + await synchroniser.accept(resourcePreview.localResource); + } + await synchroniser.apply(true, this.syncHeaders); + this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); + this._onSynchronizeResources.fire(this.synchronizingResources); + } + this.previews = []; + } + + async stop(): Promise { + for (const synchroniser of this.synchronisers) { + try { + await synchroniser.stop(); + } catch (error) { + if (!isPromiseCanceledError(error)) { + this.logService.error(error); + } + } + } + this.reset(); + } + private async performAction(resource: URI, action: (synchroniser: IUserDataSynchroniser) => Promise): Promise<[SyncResource, ISyncResourcePreview][]> { if (!this.previews) { throw new Error('Missing preview. Create preview and try again.'); @@ -516,22 +614,32 @@ class ManualSyncTask extends Disposable implements IManualSyncTask { return this.previews; } - async apply(): Promise<[SyncResource, ISyncResourcePreview][]> { + private async mergeAll(): Promise<[SyncResource, ISyncResourcePreview][]> { if (!this.previews) { - throw new Error('You need to create preview before applying'); + throw new Error('You need to create preview before merging or applying'); } if (this.synchronizingResources.length) { - throw new Error('Cannot pull while synchronizing resources'); + throw new Error('Cannot merge or apply while synchronizing resources'); } const previews: [SyncResource, ISyncResourcePreview][] = []; for (const [syncResource, preview] of this.previews) { this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); this._onSynchronizeResources.fire(this.synchronizingResources); + const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; - const newPreview = await synchroniser.apply(false, this.syncHeaders); + + /* merge those which are not yet merged */ + let newPreview: ISyncResourcePreview | null = preview; + for (const resourcePreview of preview.resourcePreviews) { + if ((resourcePreview.localChange !== Change.None || resourcePreview.remoteChange !== Change.None) && resourcePreview.mergeState === MergeState.Preview) { + newPreview = await synchroniser.merge(resourcePreview.previewResource); + } + } + if (newPreview) { previews.push(this.toSyncResourcePreview(synchroniser.resource, newPreview)); } + this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); this._onSynchronizeResources.fire(this.synchronizingResources); } @@ -539,63 +647,6 @@ class ManualSyncTask extends Disposable implements IManualSyncTask { return this.previews; } - async pull(): Promise { - if (!this.previews) { - throw new Error('You need to create preview before applying'); - } - if (this.synchronizingResources.length) { - throw new Error('Cannot pull while synchronizing resources'); - } - for (const [syncResource, preview] of this.previews) { - this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); - this._onSynchronizeResources.fire(this.synchronizingResources); - const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; - for (const resourcePreview of preview.resourcePreviews) { - const content = await synchroniser.resolveContent(resourcePreview.remoteResource) || ''; - await synchroniser.accept(resourcePreview.remoteResource, content); - } - await synchroniser.apply(true, this.syncHeaders); - this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); - this._onSynchronizeResources.fire(this.synchronizingResources); - } - this.previews = []; - } - - async push(): Promise { - if (!this.previews) { - throw new Error('You need to create preview before applying'); - } - if (this.synchronizingResources.length) { - throw new Error('Cannot pull while synchronizing resources'); - } - for (const [syncResource, preview] of this.previews) { - this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); - this._onSynchronizeResources.fire(this.synchronizingResources); - const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; - for (const resourcePreview of preview.resourcePreviews) { - const content = await synchroniser.resolveContent(resourcePreview.localResource) || ''; - await synchroniser.accept(resourcePreview.localResource, content); - } - await synchroniser.apply(true, this.syncHeaders); - this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); - this._onSynchronizeResources.fire(this.synchronizingResources); - } - this.previews = []; - } - - async stop(): Promise { - for (const synchroniser of this.synchronisers) { - try { - await synchroniser.stop(); - } catch (error) { - if (!isPromiseCanceledError(error)) { - this.logService.error(error); - } - } - } - this.reset(); - } - private async getPreviews(token: CancellationToken): Promise<[SyncResource, ISyncResourcePreview][]> { const result: [SyncResource, ISyncResourcePreview][] = []; for (const synchroniser of this.synchronisers) { diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index fd972f83e84..d17ec3b39fc 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, } from 'vs/base/common/lifecycle'; -import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle, HEADER_OPERATION_ID, HEADER_EXECUTION_ID } from 'vs/platform/userDataSync/common/userDataSync'; -import { IRequestService, asText, isSuccess, asJson } from 'vs/platform/request/common/request'; +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 { 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'; import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IProductService } from 'vs/platform/product/common/productService'; +import { IProductService, ConfigurationSyncStore } from 'vs/platform/product/common/productService'; import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; @@ -19,17 +19,118 @@ import { assign } from 'vs/base/common/objects'; import { generateUuid } from 'vs/base/common/uuid'; import { isWeb } from 'vs/base/common/platform'; import { Emitter, Event } from 'vs/base/common/event'; +import { createCancelablePromise, timeout, CancelablePromise } from 'vs/base/common/async'; +import { isString, isObject, isArray } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +const SYNC_SERVICE_URL_TYPE = 'sync.store.url.type'; +const SYNC_PREVIOUS_STORE = 'sync.previous.store'; +const DONOT_MAKE_REQUESTS_UNTIL_KEY = 'sync.donot-make-requests-until'; const USER_SESSION_ID_KEY = 'sync.user-session-id'; const MACHINE_SESSION_ID_KEY = 'sync.machine-session-id'; const REQUEST_SESSION_LIMIT = 100; const REQUEST_SESSION_INTERVAL = 1000 * 60 * 5; /* 5 minutes */ +type UserDataSyncStore = IUserDataSyncStore & { defaultType?: UserDataSyncStoreType; type?: UserDataSyncStoreType }; + +export abstract class AbstractUserDataSyncStoreManagementService extends Disposable implements IUserDataSyncStoreManagementService { + + _serviceBrand: any; + + readonly userDataSyncStore: UserDataSyncStore | undefined; + + constructor( + @IProductService protected readonly productService: IProductService, + @IConfigurationService protected readonly configurationService: IConfigurationService, + @IStorageService protected readonly storageService: IStorageService, + ) { + super(); + this.userDataSyncStore = this.toUserDataSyncStore(productService[CONFIGURATION_SYNC_STORE_KEY], configurationService.getValue(CONFIGURATION_SYNC_STORE_KEY)); + } + + protected toUserDataSyncStore(productStore: ConfigurationSyncStore | undefined, configuredStore?: ConfigurationSyncStore): UserDataSyncStore | undefined { + const value: Partial = { ...(productStore || {}), ...(configuredStore || {}) }; + if (value + && isString(value.url) + && isObject(value.authenticationProviders) + && Object.keys(value.authenticationProviders).every(authenticationProviderId => isArray(value!.authenticationProviders![authenticationProviderId].scopes)) + ) { + const syncStore = value as ConfigurationSyncStore; + const type: UserDataSyncStoreType | undefined = this.storageService.get(SYNC_SERVICE_URL_TYPE, StorageScope.GLOBAL) as UserDataSyncStoreType | undefined; + const url = configuredStore?.url + || (type === 'insiders' ? syncStore.insidersUrl : type === 'stable' ? syncStore.stableUrl : undefined) + || syncStore.url; + return { + url: URI.parse(url), + type, + defaultType: syncStore.url === syncStore.insidersUrl ? 'insiders' : syncStore.url === syncStore.stableUrl ? 'stable' : undefined, + defaultUrl: URI.parse(syncStore.url), + stableUrl: syncStore.stableUrl ? URI.parse(syncStore.stableUrl) : undefined, + insidersUrl: syncStore.insidersUrl ? URI.parse(syncStore.insidersUrl) : undefined, + authenticationProviders: Object.keys(syncStore.authenticationProviders).reduce((result, id) => { + result.push({ id, scopes: syncStore!.authenticationProviders[id].scopes }); + return result; + }, []) + }; + } + return undefined; + } + + abstract switch(type: UserDataSyncStoreType): Promise; + abstract getPreviousUserDataSyncStore(): Promise; + +} + +export class UserDataSyncStoreManagementService extends AbstractUserDataSyncStoreManagementService implements IUserDataSyncStoreManagementService { + + private readonly previousConfigurationSyncStore: ConfigurationSyncStore | undefined; + + constructor( + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService, + @IStorageService storageService: IStorageService, + @IUserDataSyncLogService logService: IUserDataSyncLogService, + ) { + super(productService, configurationService, storageService); + + const previousConfigurationSyncStore = this.storageService.get(SYNC_PREVIOUS_STORE, StorageScope.GLOBAL); + if (previousConfigurationSyncStore) { + this.previousConfigurationSyncStore = JSON.parse(previousConfigurationSyncStore); + } + + const syncStore = this.productService[CONFIGURATION_SYNC_STORE_KEY]; + if (syncStore) { + this.storageService.store(SYNC_PREVIOUS_STORE, JSON.stringify(syncStore), StorageScope.GLOBAL); + } else { + this.storageService.remove(SYNC_PREVIOUS_STORE, StorageScope.GLOBAL); + } + + if (this.userDataSyncStore) { + logService.info('Using settings sync service', this.userDataSyncStore.url.toString()); + } + } + + async switch(type: UserDataSyncStoreType): Promise { + if (type !== this.userDataSyncStore?.type) { + if (type === this.userDataSyncStore?.defaultType) { + this.storageService.remove(SYNC_SERVICE_URL_TYPE, StorageScope.GLOBAL); + } else { + this.storageService.store(SYNC_SERVICE_URL_TYPE, type, StorageScope.GLOBAL); + } + } + } + + async getPreviousUserDataSyncStore(): Promise { + return this.toUserDataSyncStore(this.previousConfigurationSyncStore); + } +} + export class UserDataSyncStoreService extends Disposable implements IUserDataSyncStoreService { _serviceBrand: any; - readonly userDataSyncStore: IUserDataSyncStore | undefined; + private readonly userDataSyncStoreUrl: URI | undefined; + private authToken: { token: string, type: string } | undefined; private readonly commonHeadersPromise: Promise<{ [key: string]: string; }>; private readonly session: RequestsSession; @@ -40,17 +141,22 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn private _onTokenSucceed: Emitter = this._register(new Emitter()); readonly onTokenSucceed: Event = this._onTokenSucceed.event; + private _donotMakeRequestsUntil: Date | undefined = undefined; + get donotMakeRequestsUntil() { return this._donotMakeRequestsUntil; } + private _onDidChangeDonotMakeRequestsUntil = this._register(new Emitter()); + readonly onDidChangeDonotMakeRequestsUntil = this._onDidChangeDonotMakeRequestsUntil.event; + constructor( @IProductService productService: IProductService, - @IConfigurationService configurationService: IConfigurationService, @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.userDataSyncStore = getUserDataSyncStore(productService, configurationService); + this.userDataSyncStoreUrl = this.userDataSyncStoreManagementService.userDataSyncStore ? joinPath(this.userDataSyncStoreManagementService.userDataSyncStore.url, 'v1') : undefined; this.commonHeadersPromise = getServiceMachineId(environmentService, fileService, storageService) .then(uuid => { const headers: IHeaders = { @@ -66,70 +172,86 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn /* A requests session that limits requests per sessions */ this.session = new RequestsSession(REQUEST_SESSION_LIMIT, REQUEST_SESSION_INTERVAL, this.requestService, this.logService); + this.initDonotMakeRequestsUntil(); } setAuthToken(token: string, type: string): void { this.authToken = { token, type }; } + private initDonotMakeRequestsUntil(): void { + const donotMakeRequestsUntil = this.storageService.getNumber(DONOT_MAKE_REQUESTS_UNTIL_KEY, StorageScope.GLOBAL); + if (donotMakeRequestsUntil && Date.now() < donotMakeRequestsUntil) { + this.setDonotMakeRequestsUntil(new Date(donotMakeRequestsUntil)); + } + } + + private resetDonotMakeRequestsUntilPromise: CancelablePromise | undefined = undefined; + private setDonotMakeRequestsUntil(donotMakeRequestsUntil: Date | undefined): void { + if (this._donotMakeRequestsUntil?.getTime() !== donotMakeRequestsUntil?.getTime()) { + this._donotMakeRequestsUntil = donotMakeRequestsUntil; + + if (this.resetDonotMakeRequestsUntilPromise) { + this.resetDonotMakeRequestsUntilPromise.cancel(); + this.resetDonotMakeRequestsUntilPromise = undefined; + } + + if (this._donotMakeRequestsUntil) { + this.storageService.store(DONOT_MAKE_REQUESTS_UNTIL_KEY, this._donotMakeRequestsUntil.getTime(), StorageScope.GLOBAL); + this.resetDonotMakeRequestsUntilPromise = createCancelablePromise(token => timeout(this._donotMakeRequestsUntil!.getTime() - Date.now(), token).then(() => this.setDonotMakeRequestsUntil(undefined))); + } else { + this.storageService.remove(DONOT_MAKE_REQUESTS_UNTIL_KEY, StorageScope.GLOBAL); + } + + this._onDidChangeDonotMakeRequestsUntil.fire(); + } + } + async getAllRefs(resource: ServerResource): Promise { - if (!this.userDataSyncStore) { + if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } - const uri = joinPath(this.userDataSyncStore.url, 'resource', resource); + const uri = joinPath(this.userDataSyncStoreUrl, 'resource', resource); const headers: IHeaders = {}; - const context = await this.request({ type: 'GET', url: uri.toString(), headers }, CancellationToken.None); - - if (!isSuccess(context)) { - throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]); - } + const context = await this.request({ type: 'GET', url: uri.toString(), headers }, [], CancellationToken.None); const result = await asJson<{ url: string, created: number }[]>(context) || []; return result.map(({ url, created }) => ({ ref: relativePath(uri, uri.with({ path: url }))!, created: created * 1000 /* Server returns in seconds */ })); } async resolveContent(resource: ServerResource, ref: string): Promise { - if (!this.userDataSyncStore) { + if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStore.url, 'resource', resource, ref).toString(); + const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource, ref).toString(); const headers: IHeaders = {}; headers['Cache-Control'] = 'no-cache'; - const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None); - - if (!isSuccess(context)) { - throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]); - } - + const context = await this.request({ type: 'GET', url, headers }, [], CancellationToken.None); const content = await asText(context); return content; } async delete(resource: ServerResource): Promise { - if (!this.userDataSyncStore) { + if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStore.url, 'resource', resource).toString(); + const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource).toString(); const headers: IHeaders = {}; - const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None); - - if (!isSuccess(context)) { - throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]); - } + await this.request({ type: 'DELETE', url, headers }, [], CancellationToken.None); } async read(resource: ServerResource, oldValue: IUserData | null, headers: IHeaders = {}): Promise { - if (!this.userDataSyncStore) { + if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStore.url, 'resource', resource, 'latest').toString(); + const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource, 'latest').toString(); headers = { ...headers }; // Disable caching as they are cached by synchronisers headers['Cache-Control'] = 'no-cache'; @@ -137,17 +259,13 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn headers['If-None-Match'] = oldValue.ref; } - const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None); + const context = await this.request({ type: 'GET', url, headers }, [304], CancellationToken.None); if (context.res.statusCode === 304) { // There is no new value. Hence return the old value. return oldValue!; } - if (!isSuccess(context)) { - throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]); - } - const ref = context.res.headers['etag']; if (!ref) { throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, context.res.headers[HEADER_OPERATION_ID]); @@ -157,22 +275,18 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } async write(resource: ServerResource, data: string, ref: string | null, headers: IHeaders = {}): Promise { - if (!this.userDataSyncStore) { + if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStore.url, 'resource', resource).toString(); + const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource).toString(); headers = { ...headers }; headers['Content-Type'] = 'text/plain'; if (ref) { headers['If-Match'] = ref; } - const context = await this.request({ type: 'POST', url, data, headers }, CancellationToken.None); - - if (!isSuccess(context)) { - throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]); - } + const context = await this.request({ type: 'POST', url, data, headers }, [], CancellationToken.None); const newRef = context.res.headers['etag']; if (!newRef) { @@ -182,18 +296,15 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } async manifest(headers: IHeaders = {}): Promise { - if (!this.userDataSyncStore) { + if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStore.url, 'manifest').toString(); + const url = joinPath(this.userDataSyncStoreUrl, 'manifest').toString(); headers = { ...headers }; headers['Content-Type'] = 'application/json'; - const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None); - if (!isSuccess(context)) { - throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]); - } + const context = await this.request({ type: 'GET', url, headers }, [], CancellationToken.None); const manifest = await asJson(context); const currentSessionId = this.storageService.get(USER_SESSION_ID_KEY, StorageScope.GLOBAL); @@ -217,18 +328,14 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } async clear(): Promise { - if (!this.userDataSyncStore) { + if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStore.url, 'resource').toString(); + const url = joinPath(this.userDataSyncStoreUrl, 'resource').toString(); const headers: IHeaders = { 'Content-Type': 'text/plain' }; - const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None); - - if (!isSuccess(context)) { - throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]); - } + await this.request({ type: 'DELETE', url, headers }, [], CancellationToken.None); // clear cached session. this.clearSession(); @@ -239,11 +346,16 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn this.storageService.remove(MACHINE_SESSION_ID_KEY, StorageScope.GLOBAL); } - private async request(options: IRequestOptions, token: CancellationToken): Promise { + private async request(options: IRequestOptions, successCodes: number[], token: CancellationToken): Promise { if (!this.authToken) { throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized, undefined); } + if (this._donotMakeRequestsUntil && Date.now() < this._donotMakeRequestsUntil.getTime()) { + throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too many requests (429).`, UserDataSyncErrorCode.TooManyRequestsAndRetryAfter, undefined); + } + this.setDonotMakeRequestsUntil(undefined); + const commonHeaders = await this.commonHeadersPromise; options.headers = assign(options.headers || {}, commonHeaders, { 'X-Account-Type': this.authToken.type, @@ -268,7 +380,8 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn const operationId = context.res.headers[HEADER_OPERATION_ID]; const requestInfo = { url: options.url, status: context.res.statusCode, 'execution-id': options.headers[HEADER_EXECUTION_ID], 'operation-id': operationId }; - if (isSuccess(context)) { + const isSuccess = isSuccessContext(context) || (context.res.statusCode && successCodes.indexOf(context.res.statusCode) !== -1); + if (isSuccess) { this.logService.trace('Request succeeded', requestInfo); } else { this.logService.info('Request failed', requestInfo); @@ -299,7 +412,17 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } if (context.res.statusCode === 429) { - throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too many requests (429).`, UserDataSyncErrorCode.TooManyRequests, operationId); + const retryAfter = context.res.headers['retry-after']; + if (retryAfter) { + this.setDonotMakeRequestsUntil(new Date(Date.now() + (parseInt(retryAfter) * 1000))); + throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too many requests (429).`, UserDataSyncErrorCode.TooManyRequestsAndRetryAfter, operationId); + } else { + throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too many requests (429).`, UserDataSyncErrorCode.TooManyRequests, operationId); + } + } + + if (!isSuccess) { + throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, operationId); } return context; diff --git a/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts index 7868c0ebc2e..81483659e9b 100644 --- a/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; import { Event } from 'vs/base/common/event'; import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { UserDataAutoSyncService as BaseUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; @@ -16,6 +16,7 @@ import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/us export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { constructor( + @IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @IUserDataSyncService userDataSyncService: IUserDataSyncService, @@ -27,7 +28,7 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { @IStorageService storageService: IStorageService, @IEnvironmentService environmentService: IEnvironmentService, ) { - super(userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, userDataSyncMachinesService, storageService, environmentService); + super(userDataSyncStoreManagementService, userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, userDataSyncMachinesService, storageService, environmentService); this._register(Event.debounce(Event.any( Event.map(electronService.onWindowFocus, () => 'windowFocus'), diff --git a/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts index a6c7b761025..2d5594c01d7 100644 --- a/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts @@ -207,33 +207,6 @@ suite('GlobalStateSync', () => { assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' } }); }); - test('first time sync - push', async () => { - updateStorage('a', 'value1', testClient); - updateStorage('b', 'value2', testClient); - - await testObject.push(); - assert.equal(testObject.status, SyncStatus.Idle); - assert.deepEqual(testObject.conflicts, []); - - const { content } = await testClient.read(testObject.resource); - assert.ok(content !== null); - const actual = parseGlobalState(content!); - assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' }, 'b': { version: 1, value: 'value2' } }); - }); - - test('first time sync - pull', async () => { - updateStorage('a', 'value1', client2); - updateStorage('b', 'value2', client2); - await client2.sync(); - - await testObject.pull(); - assert.equal(testObject.status, SyncStatus.Idle); - assert.deepEqual(testObject.conflicts, []); - - assert.equal(readStorage('a', testClient), 'value1'); - assert.equal(readStorage('b', testClient), 'value2'); - }); - function parseGlobalState(content: string): IGlobalState { const syncData: ISyncData = JSON.parse(content); return JSON.parse(syncData.content); diff --git a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts index 9fd790befce..02b0154b627 100644 --- a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts @@ -61,6 +61,45 @@ suite('KeybindingsSync', () => { assert.deepEqual(server.requests, []); }); + test('when keybindings file is empty and remote has no changes', async () => { + const fileService = client.instantiationService.get(IFileService); + const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource; + await fileService.writeFile(keybindingsResource, VSBuffer.fromString('')); + + await testObject.sync(await client.manifest()); + + 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((await fileService.readFile(keybindingsResource)).value.toString(), ''); + }); + + test('when keybindings file is empty and remote has changes', 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('')); + + await testObject.sync(await client.manifest()); + + 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((await fileService.readFile(keybindingsResource)).value.toString(), content); + }); + test('when keybindings file is created after first sync', async () => { const fileService = client.instantiationService.get(IFileService); const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource; @@ -83,4 +122,20 @@ suite('KeybindingsSync', () => { assert.equal(testObject.getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!), '[]'); }); + test('test apply remote when keybindings file does not exist', async () => { + const fileService = client.instantiationService.get(IFileService); + const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource; + if (await fileService.exists(keybindingsResource)) { + await fileService.del(keybindingsResource); + } + + const preview = (await testObject.preview(await client.manifest()))!; + + server.reset(); + const content = await testObject.resolveContent(preview.resourcePreviews[0].remoteResource); + await testObject.accept(preview.resourcePreviews[0].remoteResource, content); + await testObject.apply(false); + assert.deepEqual(server.requests, []); + }); + }); diff --git a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts index 4cd232beb27..6117c2b55e2 100644 --- a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, ISyncData } from 'vs/platform/userDataSync/common/userDataSync'; +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'; @@ -15,32 +15,30 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { Event } from 'vs/base/common/event'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -suite('SettingsSync', () => { +Registry.as(Extensions.Configuration).registerConfiguration({ + 'id': 'settingsSync', + 'type': 'object', + 'properties': { + 'settingsSync.machine': { + 'type': 'string', + 'scope': ConfigurationScope.MACHINE + }, + 'settingsSync.machineOverridable': { + 'type': 'string', + 'scope': ConfigurationScope.MACHINE_OVERRIDABLE + } + } +}); + +suite('SettingsSync - Auto', () => { const disposableStore = new DisposableStore(); const server = new UserDataSyncTestServer(); let client: UserDataSyncClient; - let testObject: SettingsSynchroniser; - suiteSetup(() => { - Registry.as(Extensions.Configuration).registerConfiguration({ - 'id': 'settingsSync', - 'type': 'object', - 'properties': { - 'settingsSync.machine': { - 'type': 'string', - 'scope': ConfigurationScope.MACHINE - }, - 'settingsSync.machineOverridable': { - 'type': 'string', - 'scope': ConfigurationScope.MACHINE_OVERRIDABLE - } - } - }); - }); - setup(async () => { client = disposableStore.add(new UserDataSyncClient(server)); await client.setUp(true); @@ -81,6 +79,61 @@ suite('SettingsSync', () => { assert.deepEqual(server.requests, []); }); + test('when settings file is empty and remote has no changes', async () => { + const fileService = client.instantiationService.get(IFileService); + const settingsResource = client.instantiationService.get(IEnvironmentService).settingsResource; + await fileService.writeFile(settingsResource, VSBuffer.fromString('')); + + await testObject.sync(await client.manifest()); + + 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((await fileService.readFile(settingsResource)).value.toString(), ''); + }); + + test('when settings file is empty and remote has changes', async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const content = + `{ + // Always + "files.autoSave": "afterDelay", + "files.simpleDialog.enable": true, + + // Workbench + "workbench.colorTheme": "GitHub Sharp", + "workbench.tree.indent": 20, + "workbench.colorCustomizations": { + "editorLineNumber.activeForeground": "#ff0000", + "[GitHub Sharp]": { + "statusBarItem.remoteBackground": "#24292E", + "editorPane.background": "#f3f1f11a" + } + }, + + "gitBranch.base": "remote-repo/master", + + // Experimental + "workbench.view.experimental.allowMovingToNewContainer": true, +}`; + await client2.instantiationService.get(IFileService).writeFile(client2.instantiationService.get(IEnvironmentService).settingsResource, VSBuffer.fromString(content)); + await client2.sync(); + + const fileService = client.instantiationService.get(IFileService); + const settingsResource = client.instantiationService.get(IEnvironmentService).settingsResource; + await fileService.writeFile(settingsResource, VSBuffer.fromString('')); + + await testObject.sync(await client.manifest()); + + 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((await fileService.readFile(settingsResource)).value.toString(), content); + }); + test('when settings file is created after first sync', async () => { const fileService = client.instantiationService.get(IFileService); @@ -128,7 +181,7 @@ suite('SettingsSync', () => { "workbench.view.experimental.allowMovingToNewContainer": true, }`; - await updateSettings(expected); + await updateSettings(expected, client); await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); @@ -151,7 +204,7 @@ suite('SettingsSync', () => { "settingsSync.machine": "someValue", "settingsSync.machineOverridable": "someValue" }`; - await updateSettings(settingsContent); + await updateSettings(settingsContent, client); await testObject.sync(await client.manifest()); @@ -182,7 +235,7 @@ suite('SettingsSync', () => { // Machine "settingsSync.machineOverridable": "someValue" }`; - await updateSettings(settingsContent); + await updateSettings(settingsContent, client); await testObject.sync(await client.manifest()); @@ -213,7 +266,7 @@ suite('SettingsSync', () => { "settingsSync.machineOverridable": "someValue", "files.simpleDialog.enable": true, }`; - await updateSettings(settingsContent); + await updateSettings(settingsContent, client); await testObject.sync(await client.manifest()); @@ -237,7 +290,7 @@ suite('SettingsSync', () => { "settingsSync.machine": "someValue", "settingsSync.machineOverridable": "someValue" }`; - await updateSettings(settingsContent); + await updateSettings(settingsContent, client); await testObject.sync(await client.manifest()); @@ -255,7 +308,7 @@ suite('SettingsSync', () => { "settingsSync.machine": "someValue", "settingsSync.machineOverridable": "someValue", }`; - await updateSettings(settingsContent); + await updateSettings(settingsContent, client); await testObject.sync(await client.manifest()); @@ -274,14 +327,14 @@ suite('SettingsSync', () => { "files.simpleDialog.enable": true, }`; - await updateSettings(content); + await updateSettings(content, client); await testObject.sync(await client.manifest()); const promise = Event.toPromise(testObject.onDidChangeLocal); await updateSettings(`{ "files.autoSave": "off", "files.simpleDialog.enable": true, -}`); +}`, client); await promise; }); @@ -302,12 +355,12 @@ suite('SettingsSync', () => { "workbench.colorTheme": "GitHub Sharp", // Ignored - "sync.ignoredSettings": [ + "settingsSync.ignoredSettings": [ "editor.fontFamily", "terminal.integrated.shell.osx" ] }`; - await updateSettings(settingsContent); + await updateSettings(settingsContent, client); await testObject.sync(await client.manifest()); @@ -323,7 +376,7 @@ suite('SettingsSync', () => { "workbench.colorTheme": "GitHub Sharp", // Ignored - "sync.ignoredSettings": [ + "settingsSync.ignoredSettings": [ "editor.fontFamily", "terminal.integrated.shell.osx" ] @@ -347,7 +400,7 @@ suite('SettingsSync', () => { "workbench.colorTheme": "GitHub Sharp", // Ignored - "sync.ignoredSettings": [ + "settingsSync.ignoredSettings": [ "editor.fontFamily", "terminal.integrated.shell.osx" ], @@ -355,7 +408,7 @@ suite('SettingsSync', () => { // Machine "settingsSync.machine": "someValue", }`; - await updateSettings(settingsContent); + await updateSettings(settingsContent, client); await testObject.sync(await client.manifest()); @@ -371,7 +424,7 @@ suite('SettingsSync', () => { "workbench.colorTheme": "GitHub Sharp", // Ignored - "sync.ignoredSettings": [ + "settingsSync.ignoredSettings": [ "editor.fontFamily", "terminal.integrated.shell.osx" ], @@ -402,7 +455,7 @@ suite('SettingsSync', () => { "workbench.view.experimental.allowMovingToNewContainer": true, }`; - await updateSettings(expected); + await updateSettings(expected, client); try { await testObject.sync(await client.manifest()); @@ -413,15 +466,109 @@ suite('SettingsSync', () => { } }); - function parseSettings(content: string): string { - const syncData: ISyncData = JSON.parse(content); - const settingsSyncContent: ISettingsSyncContent = JSON.parse(syncData.content); - return settingsSyncContent.settings; - } + test('sync when there are conflicts', async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + await updateSettings(JSON.stringify({ + 'a': 1, + 'b': 2, + 'settingsSync.ignoredSettings': ['a'] + }), client2); + await client2.sync(); - async function updateSettings(content: string): Promise { - await client.instantiationService.get(IFileService).writeFile(client.instantiationService.get(IEnvironmentService).settingsResource, VSBuffer.fromString(content)); - } + await updateSettings(JSON.stringify({ + 'a': 2, + 'b': 1, + 'settingsSync.ignoredSettings': ['a'] + }), client); + await testObject.sync(await client.manifest()); + assert.equal(testObject.status, SyncStatus.HasConflicts); + assert.equal(testObject.conflicts[0].localResource.toString(), testObject.localResource); + + const fileService = client.instantiationService.get(IFileService); + const mergeContent = (await fileService.readFile(testObject.conflicts[0].previewResource)).value.toString(); + assert.deepEqual(JSON.parse(mergeContent), { + 'b': 1, + 'settingsSync.ignoredSettings': ['a'] + }); + }); }); + +suite('SettingsSync - Manual', () => { + + const disposableStore = new DisposableStore(); + const server = new UserDataSyncTestServer(); + let client: UserDataSyncClient; + let testObject: SettingsSynchroniser; + + setup(async () => { + client = disposableStore.add(new UserDataSyncClient(server)); + await client.setUp(true); + testObject = (client.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Settings) as SettingsSynchroniser; + disposableStore.add(toDisposable(() => client.instantiationService.get(IUserDataSyncStoreService).clear())); + }); + + teardown(() => disposableStore.clear()); + + test('do not sync ignored settings', async () => { + const settingsContent = + `{ + // Always + "files.autoSave": "afterDelay", + "files.simpleDialog.enable": true, + + // Editor + "editor.fontFamily": "Fira Code", + + // Terminal + "terminal.integrated.shell.osx": "some path", + + // Workbench + "workbench.colorTheme": "GitHub Sharp", + + // Ignored + "settingsSync.ignoredSettings": [ + "editor.fontFamily", + "terminal.integrated.shell.osx" + ] +}`; + await updateSettings(settingsContent, client); + + let preview = await testObject.preview(await client.manifest()); + assert.equal(testObject.status, SyncStatus.Syncing); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + preview = await testObject.apply(false); + + const { content } = await client.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSettings(content!); + assert.deepEqual(actual, `{ + // Always + "files.autoSave": "afterDelay", + "files.simpleDialog.enable": true, + + // Workbench + "workbench.colorTheme": "GitHub Sharp", + + // Ignored + "settingsSync.ignoredSettings": [ + "editor.fontFamily", + "terminal.integrated.shell.osx" + ] +}`); + }); + +}); + +function parseSettings(content: string): string { + const syncData: ISyncData = JSON.parse(content); + const settingsSyncContent: ISettingsSyncContent = JSON.parse(syncData.content); + return settingsSyncContent.settings; +} + +async function updateSettings(content: string, client: UserDataSyncClient): Promise { + await client.instantiationService.get(IFileService).writeFile(client.instantiationService.get(IEnvironmentService).settingsResource, VSBuffer.fromString(content)); + await client.instantiationService.get(IConfigurationService).reloadConfiguration(); +} diff --git a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts index 394cfa26e3e..262fcf95f63 100644 --- a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts @@ -596,7 +596,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await testObject.sync(await testClient.manifest()); - await testObject.accept(testObject.conflicts[0].previewResource, ''); + await testObject.accept(testObject.conflicts[0].previewResource, null); await testObject.apply(false); assert.equal(testObject.status, SyncStatus.Idle); @@ -613,35 +613,6 @@ suite('SnippetsSync', () => { assert.deepEqual(actual, { 'typescript.json': tsSnippet1 }); }); - test('first time sync - push', async () => { - await updateSnippet('html.json', htmlSnippet1, testClient); - await updateSnippet('typescript.json', tsSnippet1, testClient); - - await testObject.push(); - assert.equal(testObject.status, SyncStatus.Idle); - assert.deepEqual(testObject.conflicts, []); - - const { content } = await testClient.read(testObject.resource); - assert.ok(content !== null); - const actual = parseSnippets(content!); - assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }); - }); - - test('first time sync - pull', async () => { - await updateSnippet('html.json', htmlSnippet1, client2); - await updateSnippet('typescript.json', tsSnippet1, client2); - await client2.sync(); - - await testObject.pull(); - assert.equal(testObject.status, SyncStatus.Idle); - assert.deepEqual(testObject.conflicts, []); - - const actual1 = await readSnippet('html.json', testClient); - assert.equal(actual1, htmlSnippet1); - const actual2 = await readSnippet('typescript.json', testClient); - assert.equal(actual2, tsSnippet1); - }); - test('sync global and language snippet', async () => { await updateSnippet('global.code-snippets', globalSnippet, client2); await updateSnippet('html.json', htmlSnippet1, client2); diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index 0d5106c1945..5d2731d5e13 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -4,18 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncResourceEnablementService, IRemoteUserData, ISyncData, Change, USER_DATA_SYNC_SCHEME, IUserDataManifest, MergeState } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncResourceEnablementService, IRemoteUserData, Change, USER_DATA_SYNC_SCHEME, IUserDataManifest, MergeState, IResourcePreview as IBaseResourcePreview } 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 { AbstractSynchroniser, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { Barrier } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { isEqual, joinPath } from 'vs/base/common/resources'; -const resource = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'testResource', path: `/current.json` }); +interface ITestResourcePreview extends IResourcePreview { + ref: string; +} class TestSynchroniser extends AbstractSynchroniser { @@ -28,6 +32,7 @@ class TestSynchroniser extends AbstractSynchroniser { protected readonly version: number = 1; private cancelled: boolean = false; + readonly localResource = joinPath(this.environmentService.userRoamingDataHome, 'testResource.json'); protected getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise { if (this.failWhenGettingLatestRemoteUserData) { @@ -48,33 +53,95 @@ class TestSynchroniser extends AbstractSynchroniser { return super.doSync(remoteUserData, lastSyncUserData, apply); } - protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { - return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, acceptedContent: remoteUserData.ref, previewContent: remoteUserData.ref, previewResource: resource, acceptedResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }]; - } - - protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { - return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, acceptedContent: remoteUserData.ref, previewContent: remoteUserData.ref, previewResource: resource, acceptedResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }]; - } - - protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { - return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, acceptedContent: remoteUserData.ref, previewContent: remoteUserData.ref, previewResource: resource, acceptedResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }]; - } - - protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { if (this.syncResult.hasError) { throw new Error('failed'); } - return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, acceptedContent: remoteUserData.ref, previewContent: remoteUserData.ref, previewResource: resource, acceptedResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }]; + + let fileContent = null; + try { + fileContent = await this.fileService.readFile(this.localResource); + } catch (error) { } + + return [{ + localResource: this.localResource, + localContent: fileContent ? fileContent.value.toString() : null, + remoteResource: this.localResource.with(({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' })), + remoteContent: remoteUserData.syncData ? remoteUserData.syncData.content : null, + previewResource: this.localResource.with(({ scheme: USER_DATA_SYNC_SCHEME, authority: 'preview' })), + ref: remoteUserData.ref, + localChange: Change.Modified, + remoteChange: Change.Modified, + acceptedResource: this.localResource.with(({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })), + }]; } - protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, preview: IResourcePreview[], forcePush: boolean): Promise { - if (preview[0]?.acceptedContent) { - await this.applyRef(preview[0].acceptedContent); + protected async getMergeResult(resourcePreview: ITestResourcePreview, token: CancellationToken): Promise { + return { + content: resourcePreview.ref, + localChange: Change.Modified, + remoteChange: Change.Modified, + hasConflicts: this.syncResult.hasConflicts, + }; + } + + protected async getAcceptResult(resourcePreview: ITestResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise { + + if (isEqual(resource, resourcePreview.localResource)) { + return { + content: resourcePreview.localContent, + localChange: Change.None, + remoteChange: resourcePreview.localContent === null ? Change.Deleted : Change.Modified, + }; + } + + if (isEqual(resource, resourcePreview.remoteResource)) { + return { + content: resourcePreview.remoteContent, + localChange: resourcePreview.remoteContent === null ? Change.Deleted : Change.Modified, + remoteChange: Change.None, + }; + } + + if (isEqual(resource, resourcePreview.previewResource)) { + if (content === undefined) { + return { + content: resourcePreview.ref, + localChange: Change.Modified, + remoteChange: Change.Modified, + }; + } else { + return { + content, + localChange: content === null ? resourcePreview.localContent !== null ? Change.Deleted : Change.None : Change.Modified, + remoteChange: content === null ? resourcePreview.remoteContent !== null ? Change.Deleted : Change.None : Change.Modified, + }; + } + } + + throw new Error(`Invalid Resource: ${resource.toString()}`); + } + + protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [IResourcePreview, IAcceptResult][], force: boolean): Promise { + if (resourcePreviews[0][1].localChange === Change.Deleted) { + await this.fileService.del(this.localResource); + } + + if (resourcePreviews[0][1].localChange === Change.Added || resourcePreviews[0][1].localChange === Change.Modified) { + await this.fileService.writeFile(this.localResource, VSBuffer.fromString(resourcePreviews[0][1].content!)); + } + + if (resourcePreviews[0][1].remoteChange === Change.Deleted) { + await this.applyRef(null, remoteUserData.ref); + } + + if (resourcePreviews[0][1].remoteChange === Change.Added || resourcePreviews[0][1].remoteChange === Change.Modified) { + await this.applyRef(resourcePreviews[0][1].content, remoteUserData.ref); } } - async applyRef(ref: string): Promise { - const remoteUserData = await this.updateRemoteUserData('', ref); + async applyRef(content: string | null, ref: string): Promise { + const remoteUserData = await this.updateRemoteUserData(content === null ? '' : content, ref); await this.updateLastSyncUserData(remoteUserData); } @@ -96,7 +163,7 @@ class TestSynchroniser extends AbstractSynchroniser { } -suite('TestSynchronizer', () => { +suite('TestSynchronizer - Auto Sync', () => { const disposableStore = new DisposableStore(); const server = new UserDataSyncTestServer(); @@ -167,7 +234,7 @@ suite('TestSynchronizer', () => { await testObject.sync(await client.manifest()); assert.deepEqual(testObject.status, SyncStatus.HasConflicts); - assertConflicts(testObject.conflicts, [resource]); + assertConflicts(testObject.conflicts, [testObject.localResource]); }); test('sync should not run if syncing already', async () => { @@ -214,6 +281,155 @@ suite('TestSynchronizer', () => { assert.deepEqual(testObject.status, SyncStatus.HasConflicts); }); + test('accept preview during conflicts', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); + + await testObject.sync(await client.manifest()); + assert.deepEqual(testObject.status, SyncStatus.HasConflicts); + + await testObject.accept(testObject.conflicts[0].previewResource); + assert.deepEqual(testObject.status, SyncStatus.Syncing); + assertConflicts(testObject.conflicts, []); + + await testObject.apply(false); + assert.deepEqual(testObject.status, SyncStatus.Idle); + const fileService = client.instantiationService.get(IFileService); + assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, (await fileService.readFile(testObject.localResource)).value.toString()); + }); + + test('accept remote during conflicts', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncBarrier.open(); + await testObject.sync(await client.manifest()); + const fileService = client.instantiationService.get(IFileService); + const currentRemoteContent = (await testObject.getRemoteUserData(null)).syncData?.content; + const newLocalContent = 'conflict'; + await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent)); + + testObject.syncResult = { hasConflicts: true, hasError: false }; + await testObject.sync(await client.manifest()); + assert.deepEqual(testObject.status, SyncStatus.HasConflicts); + + await testObject.accept(testObject.conflicts[0].remoteResource); + assert.deepEqual(testObject.status, SyncStatus.Syncing); + assertConflicts(testObject.conflicts, []); + + await testObject.apply(false); + assert.deepEqual(testObject.status, SyncStatus.Idle); + assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, currentRemoteContent); + assert.equal((await fileService.readFile(testObject.localResource)).value.toString(), currentRemoteContent); + }); + + test('accept local during conflicts', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncBarrier.open(); + await testObject.sync(await client.manifest()); + const fileService = client.instantiationService.get(IFileService); + const newLocalContent = 'conflict'; + await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent)); + + testObject.syncResult = { hasConflicts: true, hasError: false }; + await testObject.sync(await client.manifest()); + assert.deepEqual(testObject.status, SyncStatus.HasConflicts); + + await testObject.accept(testObject.conflicts[0].localResource); + assert.deepEqual(testObject.status, SyncStatus.Syncing); + assertConflicts(testObject.conflicts, []); + + await testObject.apply(false); + assert.deepEqual(testObject.status, SyncStatus.Idle); + assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, newLocalContent); + assert.equal((await fileService.readFile(testObject.localResource)).value.toString(), newLocalContent); + }); + + test('accept new content during conflicts', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncBarrier.open(); + await testObject.sync(await client.manifest()); + const fileService = client.instantiationService.get(IFileService); + const newLocalContent = 'conflict'; + await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent)); + + testObject.syncResult = { hasConflicts: true, hasError: false }; + await testObject.sync(await client.manifest()); + assert.deepEqual(testObject.status, SyncStatus.HasConflicts); + + const mergeContent = 'newContent'; + await testObject.accept(testObject.conflicts[0].previewResource, mergeContent); + assert.deepEqual(testObject.status, SyncStatus.Syncing); + assertConflicts(testObject.conflicts, []); + + await testObject.apply(false); + assert.deepEqual(testObject.status, SyncStatus.Idle); + assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, mergeContent); + assert.equal((await fileService.readFile(testObject.localResource)).value.toString(), mergeContent); + }); + + test('accept delete during conflicts', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncBarrier.open(); + await testObject.sync(await client.manifest()); + const fileService = client.instantiationService.get(IFileService); + const newLocalContent = 'conflict'; + await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent)); + + testObject.syncResult = { hasConflicts: true, hasError: false }; + await testObject.sync(await client.manifest()); + assert.deepEqual(testObject.status, SyncStatus.HasConflicts); + + await testObject.accept(testObject.conflicts[0].previewResource, null); + assert.deepEqual(testObject.status, SyncStatus.Syncing); + assertConflicts(testObject.conflicts, []); + + await testObject.apply(false); + assert.deepEqual(testObject.status, SyncStatus.Idle); + assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, ''); + assert.ok(!(await fileService.exists(testObject.localResource))); + }); + + test('accept deleted local during conflicts', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncBarrier.open(); + await testObject.sync(await client.manifest()); + const fileService = client.instantiationService.get(IFileService); + await fileService.del(testObject.localResource); + + testObject.syncResult = { hasConflicts: true, hasError: false }; + await testObject.sync(await client.manifest()); + assert.deepEqual(testObject.status, SyncStatus.HasConflicts); + + await testObject.accept(testObject.conflicts[0].localResource); + assert.deepEqual(testObject.status, SyncStatus.Syncing); + assertConflicts(testObject.conflicts, []); + + await testObject.apply(false); + assert.deepEqual(testObject.status, SyncStatus.Idle); + assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, ''); + assert.ok(!(await fileService.exists(testObject.localResource))); + }); + + test('accept deleted remote during conflicts', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncBarrier.open(); + const fileService = client.instantiationService.get(IFileService); + await fileService.writeFile(testObject.localResource, VSBuffer.fromString('some content')); + testObject.syncResult = { hasConflicts: true, hasError: false }; + + await testObject.sync(await client.manifest()); + assert.deepEqual(testObject.status, SyncStatus.HasConflicts); + + await testObject.accept(testObject.conflicts[0].remoteResource); + assert.deepEqual(testObject.status, SyncStatus.Syncing); + assertConflicts(testObject.conflicts, []); + + await testObject.apply(false); + assert.deepEqual(testObject.status, SyncStatus.Idle); + assert.equal((await testObject.getRemoteUserData(null)).syncData, null); + assert.ok(!(await fileService.exists(testObject.localResource))); + }); + test('request latest data on precondition failure', async () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); // Sync once @@ -224,7 +440,7 @@ suite('TestSynchronizer', () => { // update remote data before syncing so that 412 is thrown by server const disposable = testObject.onDoSyncCall.event(async () => { disposable.dispose(); - await testObject.applyRef(ref); + await testObject.applyRef(ref, ref); server.reset(); testObject.syncBarrier.open(); }); @@ -266,8 +482,26 @@ suite('TestSynchronizer', () => { assert.equal(testObject.status, SyncStatus.Idle); }); +}); - test('preview: status is set to syncing when asked for preview if there are no conflicts', async () => { +suite('TestSynchronizer - Manual Sync', () => { + + const disposableStore = new DisposableStore(); + const server = new UserDataSyncTestServer(); + let client: UserDataSyncClient; + let userDataSyncStoreService: IUserDataSyncStoreService; + + setup(async () => { + client = disposableStore.add(new UserDataSyncClient(server)); + await client.setUp(); + userDataSyncStoreService = client.instantiationService.get(IUserDataSyncStoreService); + disposableStore.add(toDisposable(() => userDataSyncStoreService.clear())); + client.instantiationService.get(IFileService).registerProvider(USER_DATA_SYNC_SCHEME, new InMemoryFileSystemProvider()); + }); + + teardown(() => disposableStore.clear()); + + test('preview', async () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -275,11 +509,11 @@ suite('TestSynchronizer', () => { const preview = await testObject.preview(await client.manifest()); assert.deepEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [resource]); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); assertConflicts(testObject.conflicts, []); }); - test('preview: status is syncing after merging if there are no conflicts', async () => { + test('preview -> merge', async () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -288,26 +522,134 @@ suite('TestSynchronizer', () => { preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); assert.deepEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [resource]); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); assertConflicts(testObject.conflicts, []); }); - test('preview: status is set to idle after merging and applying if there are no conflicts', async () => { + test('preview -> accept', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.manifest()); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + + assert.deepEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); + assertConflicts(testObject.conflicts, []); + }); + + test('preview -> merge -> accept', async () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); let preview = await testObject.preview(await client.manifest()); preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].localResource); + + assert.deepEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); + assertConflicts(testObject.conflicts, []); + }); + + test('preview -> merge -> apply', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.manifest()); + + const manifest = await client.manifest(); + let preview = await testObject.preview(manifest); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); preview = await testObject.apply(false); assert.deepEqual(testObject.status, SyncStatus.Idle); assert.equal(preview, null); assertConflicts(testObject.conflicts, []); + + const expectedContent = manifest!.latest![testObject.resource]; + assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.equal((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); }); - test('preview: discarding the merge', async () => { + test('preview -> accept -> apply', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.manifest()); + + const manifest = await client.manifest(); + const expectedContent = manifest!.latest![testObject.resource]; + let preview = await testObject.preview(manifest); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + preview = await testObject.apply(false); + + assert.deepEqual(testObject.status, SyncStatus.Idle); + assert.equal(preview, null); + assertConflicts(testObject.conflicts, []); + + assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.equal((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); + }); + + test('preview -> merge -> accept -> apply', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.manifest()); + + const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); + let preview = await testObject.preview(await client.manifest()); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].localResource); + preview = await testObject.apply(false); + + assert.deepEqual(testObject.status, SyncStatus.Idle); + assert.equal(preview, null); + assertConflicts(testObject.conflicts, []); + + assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.equal(!(await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); + }); + + test('preview -> accept', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.manifest()); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + + assert.deepEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assertConflicts(testObject.conflicts, []); + }); + + test('preview -> accept -> apply', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.manifest()); + + const manifest = await client.manifest(); + const expectedContent = manifest!.latest![testObject.resource]; + let preview = await testObject.preview(await client.manifest()); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + preview = await testObject.apply(false); + + assert.deepEqual(testObject.status, SyncStatus.Idle); + assert.equal(preview, null); + assertConflicts(testObject.conflicts, []); + + assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.equal((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); + }); + + test('preivew -> merge -> discard', async () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -317,39 +659,155 @@ suite('TestSynchronizer', () => { preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); assert.deepEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [resource]); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview); assertConflicts(testObject.conflicts, []); }); - test('preview: status is syncing after accepting when there are no conflicts', async () => { + test('preivew -> merge -> discard -> accept', async () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); let preview = await testObject.preview(await client.manifest()); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].acceptedContent!); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); assert.deepEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [resource]); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); assertConflicts(testObject.conflicts, []); }); - test('preview: status is set to idle and sync is applied after accepting when there are no conflicts before merging', async () => { + test('preivew -> accept -> discard', async () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); let preview = await testObject.preview(await client.manifest()); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].acceptedContent!); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + + assert.deepEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview); + assertConflicts(testObject.conflicts, []); + }); + + test('preivew -> accept -> discard -> accept', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.manifest()); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); + + assert.deepEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); + assertConflicts(testObject.conflicts, []); + }); + + test('preivew -> accept -> discard -> merge', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.manifest()); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource); + + assert.deepEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); + assertConflicts(testObject.conflicts, []); + }); + + test('preivew -> merge -> accept -> discard', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.manifest()); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + + assert.deepEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview); + assertConflicts(testObject.conflicts, []); + }); + + test('preivew -> merge -> discard -> accept -> apply', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.manifest()); + + const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); + let preview = await testObject.preview(await client.manifest()); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].localResource); preview = await testObject.apply(false); assert.deepEqual(testObject.status, SyncStatus.Idle); assert.equal(preview, null); assertConflicts(testObject.conflicts, []); + assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.equal(!(await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); }); - test('preview: status is set to syncing when asked for preview if there are conflicts', async () => { + test('preivew -> accept -> discard -> accept -> apply', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.manifest()); + + const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); + let preview = await testObject.preview(await client.manifest()); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].localResource); + preview = await testObject.apply(false); + + assert.deepEqual(testObject.status, SyncStatus.Idle); + assert.equal(preview, null); + assertConflicts(testObject.conflicts, []); + assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.equal(!(await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); + }); + + test('preivew -> accept -> discard -> merge -> apply', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.manifest()); + + const manifest = await client.manifest(); + const expectedContent = manifest!.latest![testObject.resource]; + let preview = await testObject.preview(manifest); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.merge(preview!.resourcePreviews[0].localResource); + preview = await testObject.apply(false); + + assert.deepEqual(testObject.status, SyncStatus.Idle); + assert.equal(preview, null); + assertConflicts(testObject.conflicts, []); + + assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.equal((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); + }); + + test('conflicts: preview', async () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -357,11 +815,11 @@ suite('TestSynchronizer', () => { const preview = await testObject.preview(await client.manifest()); assert.deepEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [resource]); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); assertConflicts(testObject.conflicts, []); }); - test('preview: status is set to hasConflicts after merging', async () => { + test('conflicts: preview -> merge', async () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -370,12 +828,12 @@ suite('TestSynchronizer', () => { preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); assert.deepEqual(testObject.status, SyncStatus.HasConflicts); - assertPreviews(preview!.resourcePreviews, [resource]); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Conflict); - assertConflicts(testObject.conflicts, [preview!.resourcePreviews[0].previewResource]); + assertConflicts(testObject.conflicts, [preview!.resourcePreviews[0].localResource]); }); - test('preview: discarding the conflict', async () => { + test('conflicts: preview -> merge -> discard', async () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -385,73 +843,242 @@ suite('TestSynchronizer', () => { await testObject.discard(preview!.resourcePreviews[0].previewResource); assert.deepEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [resource]); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview); assertConflicts(testObject.conflicts, []); }); - test('preview: status is syncing after accepting when there are conflicts', async () => { + test('conflicts: preview -> accept', async () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); let preview = await testObject.preview(await client.manifest()); await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].acceptedContent!); + const content = await testObject.resolveContent(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, content); assert.deepEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [resource]); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); assert.deepEqual(testObject.conflicts, []); }); - test('preview: status is set to idle and sync is applied after accepting when there are conflicts', async () => { + test('conflicts: preview -> merge -> accept -> apply', async () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); - testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); + await testObject.sync(await client.manifest()); + + testObject.syncResult = { hasConflicts: true, hasError: false }; + const manifest = await client.manifest(); + const expectedContent = manifest!.latest![testObject.resource]; + let preview = await testObject.preview(manifest); - let preview = await testObject.preview(await client.manifest()); await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].acceptedContent!); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); preview = await testObject.apply(false); assert.deepEqual(testObject.status, SyncStatus.Idle); assert.equal(preview, null); assertConflicts(testObject.conflicts, []); + + assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.equal((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); }); - test('preview: status is set to syncing after accepting when there are conflicts before merging', async () => { + test('conflicts: preview -> accept', async () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); let preview = await testObject.preview(await client.manifest()); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].acceptedContent!); + const content = await testObject.resolveContent(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, content); assert.deepEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [resource]); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); assertConflicts(testObject.conflicts, []); }); - test('preview: status is set to idle and sync is applied after accepting when there are conflicts before merging', async () => { + test('conflicts: preview -> accept -> apply', async () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); - testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); + await testObject.sync(await client.manifest()); - let preview = await testObject.preview(await client.manifest()); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].acceptedContent!); + testObject.syncResult = { hasConflicts: true, hasError: false }; + const manifest = await client.manifest(); + const expectedContent = manifest!.latest![testObject.resource]; + let preview = await testObject.preview(manifest); + + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); preview = await testObject.apply(false); assert.deepEqual(testObject.status, SyncStatus.Idle); assert.equal(preview, null); assertConflicts(testObject.conflicts, []); + + assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.equal((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); }); - function assertConflicts(actual: IResourcePreview[], expected: URI[]) { - assert.deepEqual(actual.map(({ previewResource }) => previewResource.toString()), expected.map(uri => uri.toString())); - } + test('conflicts: preivew -> merge -> discard', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); - function assertPreviews(actual: IResourcePreview[], expected: URI[]) { - assert.deepEqual(actual.map(({ previewResource }) => previewResource.toString()), expected.map(uri => uri.toString())); - } + let preview = await testObject.preview(await client.manifest()); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + + assert.deepEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview); + assertConflicts(testObject.conflicts, []); + }); + + test('conflicts: preivew -> merge -> discard -> accept', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.manifest()); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); + + assert.deepEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); + assertConflicts(testObject.conflicts, []); + }); + + test('conflicts: preivew -> accept -> discard', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.manifest()); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + + assert.deepEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview); + assertConflicts(testObject.conflicts, []); + }); + + test('conflicts: preivew -> accept -> discard -> accept', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.manifest()); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); + + assert.deepEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); + assertConflicts(testObject.conflicts, []); + }); + + test('conflicts: preivew -> accept -> discard -> merge', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.manifest()); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource); + + assert.deepEqual(testObject.status, SyncStatus.HasConflicts); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Conflict); + assertConflicts(testObject.conflicts, [preview!.resourcePreviews[0].localResource]); + }); + + test('conflicts: preivew -> merge -> discard -> merge', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.manifest()); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource); + + assert.deepEqual(testObject.status, SyncStatus.HasConflicts); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Conflict); + assertConflicts(testObject.conflicts, [preview!.resourcePreviews[0].localResource]); + }); + + test('conflicts: preivew -> merge -> accept -> discard', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.manifest()); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + + assert.deepEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview); + assertConflicts(testObject.conflicts, []); + }); + + test('conflicts: preivew -> merge -> discard -> accept -> apply', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.manifest()); + + const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); + let preview = await testObject.preview(await client.manifest()); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].localResource); + preview = await testObject.apply(false); + + assert.deepEqual(testObject.status, SyncStatus.Idle); + assert.equal(preview, null); + assertConflicts(testObject.conflicts, []); + assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.equal(!(await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); + }); + + test('conflicts: preivew -> accept -> discard -> accept -> apply', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.manifest()); + + const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); + let preview = await testObject.preview(await client.manifest()); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].localResource); + preview = await testObject.apply(false); + + assert.deepEqual(testObject.status, SyncStatus.Idle); + assert.equal(preview, null); + assertConflicts(testObject.conflicts, []); + assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.equal(!(await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); + }); }); + +function assertConflicts(actual: IBaseResourcePreview[], expected: URI[]) { + assert.deepEqual(actual.map(({ localResource }) => localResource.toString()), expected.map(uri => uri.toString())); +} + +function assertPreviews(actual: IBaseResourcePreview[], expected: URI[]) { + assert.deepEqual(actual.map(({ localResource }) => localResource.toString()), expected.map(uri => uri.toString())); +} diff --git a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts index 4626457a03c..310c69dd6cf 100644 --- a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts @@ -383,4 +383,22 @@ suite('UserDataAutoSyncService', () => { assert.deepEqual((e).code, UserDataSyncErrorCode.TooManyRequests); }); + test('test auto sync is suspended when server donot accepts requests', 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); + + while (target.requests.length < 5) { + await testObject.sync(); + } + + target.reset(); + await testObject.sync(); + + assert.deepEqual(target.requests, []); + }); + }); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 0be19fe237d..767fbbf6cb5 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -6,14 +6,14 @@ 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 } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource, ServerResource, IUserDataSyncStoreManagementService } 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'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { NullLogService, ILogService } from 'vs/platform/log/common/log'; -import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; +import { UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IFileService } from 'vs/platform/files/common/files'; @@ -49,14 +49,15 @@ export class UserDataSyncClient extends Disposable { } async setUp(empty: boolean = false): Promise { - const userDataDirectory = URI.file('userdata').with({ scheme: Schemas.inMemory }); - const userDataSyncHome = joinPath(userDataDirectory, '.sync'); + const userRoamingDataHome = URI.file('userdata').with({ scheme: Schemas.inMemory }); + const userDataSyncHome = joinPath(userRoamingDataHome, '.sync'); const environmentService = this.instantiationService.stub(IEnvironmentService, >{ userDataSyncHome, - settingsResource: joinPath(userDataDirectory, 'settings.json'), - keybindingsResource: joinPath(userDataDirectory, 'keybindings.json'), - snippetsHome: joinPath(userDataDirectory, 'snippets'), - argvResource: joinPath(userDataDirectory, 'argv.json'), + userRoamingDataHome, + settingsResource: joinPath(userRoamingDataHome, 'settings.json'), + keybindingsResource: joinPath(userRoamingDataHome, 'keybindings.json'), + snippetsHome: joinPath(userRoamingDataHome, 'snippets'), + argvResource: joinPath(userRoamingDataHome, 'argv.json'), sync: 'on', }); @@ -86,6 +87,7 @@ export class UserDataSyncClient extends Disposable { this.instantiationService.stub(IUserDataSyncLogService, logService); this.instantiationService.stub(ITelemetryService, NullTelemetryService); + this.instantiationService.stub(IUserDataSyncStoreManagementService, this.instantiationService.createInstance(UserDataSyncStoreManagementService)); this.instantiationService.stub(IUserDataSyncStoreService, this.instantiationService.createInstance(UserDataSyncStoreService)); const userDataSyncAccountService: IUserDataSyncAccountService = this.instantiationService.createInstance(UserDataSyncAccountService); @@ -154,13 +156,13 @@ export class UserDataSyncTestServer implements IRequestService { get responses(): { status: number }[] { return this._responses; } reset(): void { this._requests = []; this._responses = []; this._requestsWithAllHeaders = []; } - constructor(private readonly rateLimit = Number.MAX_SAFE_INTEGER) { } + constructor(private readonly rateLimit = Number.MAX_SAFE_INTEGER, private readonly retryAfter?: number) { } async resolveProxy(url: string): Promise { return url; } async request(options: IRequestOptions, token: CancellationToken): Promise { if (this._requests.length === this.rateLimit) { - return this.toResponse(429); + return this.toResponse(429, this.retryAfter ? { 'retry-after': `${this.retryAfter}` } : undefined); } const headers: IHeaders = {}; if (options.headers) { diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts index def897833ec..768592a3ec5 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts @@ -76,65 +76,6 @@ suite('UserDataSyncService', () => { }); - test('test first time sync from the client with no changes - pull', async () => { - const target = new UserDataSyncTestServer(); - - // Setup and sync from the first client - const client = disposableStore.add(new UserDataSyncClient(target)); - await client.setUp(); - await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run(); - - // Setup the test client - const testClient = disposableStore.add(new UserDataSyncClient(target)); - await testClient.setUp(); - const testObject = testClient.instantiationService.get(IUserDataSyncService); - - // Sync (pull) from the test client - target.reset(); - await testObject.pull(); - - assert.deepEqual(target.requests, [ - { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, - ]); - - }); - - test('test first time sync from the client with changes - pull', async () => { - const target = new UserDataSyncTestServer(); - - // Setup and sync from the first client - const client = disposableStore.add(new UserDataSyncClient(target)); - await client.setUp(); - await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run(); - - // Setup the test client with changes - const testClient = disposableStore.add(new UserDataSyncClient(target)); - await testClient.setUp(); - const testObject = testClient.instantiationService.get(IUserDataSyncService); - const fileService = testClient.instantiationService.get(IFileService); - const environmentService = testClient.instantiationService.get(IEnvironmentService); - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); - await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }]))); - await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' }))); - - // Sync (pull) from the test client - target.reset(); - await testObject.pull(); - - assert.deepEqual(target.requests, [ - { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, - ]); - - }); - test('test first time sync from the client with no changes - merge', async () => { const target = new UserDataSyncTestServer(); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts index b65dbb71836..01cea2937df 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts @@ -4,17 +4,64 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IUserDataSyncStoreService, SyncResource, UserDataSyncErrorCode, UserDataSyncStoreError } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncStoreService, SyncResource, UserDataSyncErrorCode, UserDataSyncStoreError, IUserDataSyncStoreManagementService, IUserDataSyncStore } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IProductService } from 'vs/platform/product/common/productService'; +import { IProductService, ConfigurationSyncStore } from 'vs/platform/product/common/productService'; import { isWeb } from 'vs/base/common/platform'; -import { RequestsSession } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; +import { RequestsSession, UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IRequestService } from 'vs/platform/request/common/request'; -import { newWriteableBufferStream } from 'vs/base/common/buffer'; +import { newWriteableBufferStream, VSBuffer } from 'vs/base/common/buffer'; import { timeout } from 'vs/base/common/async'; import { NullLogService } from 'vs/platform/log/common/log'; +import { Event } from 'vs/base/common/event'; +import product from 'vs/platform/product/common/product'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { URI } from 'vs/base/common/uri'; + +suite('UserDataSyncStoreManagementService', () => { + const disposableStore = new DisposableStore(); + + teardown(() => disposableStore.clear()); + + test('test sync store is read from settings', async () => { + const client = disposableStore.add(new UserDataSyncClient(new UserDataSyncTestServer())); + await client.setUp(); + + client.instantiationService.stub(IProductService, { + _serviceBrand: undefined, ...product, ...{ + 'configurationSync.store': undefined + } + }); + + const configuredStore: ConfigurationSyncStore = { + url: 'http://configureHost:3000', + authenticationProviders: { 'configuredAuthProvider': { scopes: [] } } + }; + await client.instantiationService.get(IFileService).writeFile(client.instantiationService.get(IEnvironmentService).settingsResource, VSBuffer.fromString(JSON.stringify({ + 'configurationSync.store': configuredStore + }))); + await client.instantiationService.get(IConfigurationService).reloadConfiguration(); + + const expected: IUserDataSyncStore = { + url: URI.parse('http://configureHost:3000'), + defaultUrl: URI.parse('http://configureHost:3000'), + stableUrl: undefined, + insidersUrl: undefined, + authenticationProviders: [{ id: 'configuredAuthProvider', scopes: [] }] + }; + + const testObject: IUserDataSyncStoreManagementService = client.instantiationService.createInstance(UserDataSyncStoreManagementService); + + assert.equal(testObject.userDataSyncStore?.url.toString(), expected.url.toString()); + assert.equal(testObject.userDataSyncStore?.defaultUrl.toString(), expected.defaultUrl.toString()); + assert.deepEqual(testObject.userDataSyncStore?.authenticationProviders, expected.authenticationProviders); + }); + +}); suite('UserDataSyncStoreService', () => { @@ -322,6 +369,85 @@ suite('UserDataSyncStoreService', () => { assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined); }); + test('test rate limit on server with retry after', async () => { + const target = new UserDataSyncTestServer(1, 1); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncStoreService); + + await testObject.manifest(); + + const promise = Event.toPromise(testObject.onDidChangeDonotMakeRequestsUntil); + try { + await testObject.manifest(); + assert.fail('should fail'); + } catch (e) { + assert.ok(e instanceof UserDataSyncStoreError); + assert.deepEqual((e).code, UserDataSyncErrorCode.TooManyRequestsAndRetryAfter); + await promise; + assert.ok(!!testObject.donotMakeRequestsUntil); + } + }); + + test('test donotMakeRequestsUntil is reset after retry time is finished', async () => { + const client = disposableStore.add(new UserDataSyncClient(new UserDataSyncTestServer(1, 0.25))); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncStoreService); + + await testObject.manifest(); + try { + await testObject.manifest(); + } catch (e) { } + + const promise = Event.toPromise(testObject.onDidChangeDonotMakeRequestsUntil); + await timeout(300); + await promise; + assert.ok(!testObject.donotMakeRequestsUntil); + }); + + test('test donotMakeRequestsUntil is retrieved', async () => { + const client = disposableStore.add(new UserDataSyncClient(new UserDataSyncTestServer(1, 1))); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncStoreService); + + await testObject.manifest(); + try { + await testObject.manifest(); + } catch (e) { } + + const target = client.instantiationService.createInstance(UserDataSyncStoreService); + assert.equal(target.donotMakeRequestsUntil?.getTime(), testObject.donotMakeRequestsUntil?.getTime()); + }); + + test('test donotMakeRequestsUntil is checked and reset after retreived', async () => { + const client = disposableStore.add(new UserDataSyncClient(new UserDataSyncTestServer(1, 0.25))); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncStoreService); + + await testObject.manifest(); + try { + await testObject.manifest(); + } catch (e) { } + + await timeout(300); + const target = client.instantiationService.createInstance(UserDataSyncStoreService); + assert.ok(!target.donotMakeRequestsUntil); + }); + + test('test read resource request handles 304', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + await client.sync(); + + const testObject = client.instantiationService.get(IUserDataSyncStoreService); + const expected = await testObject.read(SyncResource.Settings, null); + const actual = await testObject.read(SyncResource.Settings, expected); + + assert.equal(actual, expected); + }); + }); suite('UserDataSyncRequestsSession', () => { diff --git a/src/vs/platform/webview/common/resourceLoader.ts b/src/vs/platform/webview/common/resourceLoader.ts index 22cc0e2e0c6..ee64c8ef4ff 100644 --- a/src/vs/platform/webview/common/resourceLoader.ts +++ b/src/vs/platform/webview/common/resourceLoader.ts @@ -99,7 +99,9 @@ function normalizeRequestPath(requestUri: URI) { // // vscode-webview-resource://id/scheme//authority?/path // - const resourceUri = URI.parse(requestUri.path.replace(/^\/([a-z0-9\-]+)(\/{1,2})/i, (_: string, scheme: string, sep: string) => { + + // Encode requestUri.path so that URI.parse can properly parse special characters like '#', '?', etc. + const resourceUri = URI.parse(encodeURIComponent(requestUri.path).replace(/%2F/gi, '/').replace(/^\/([a-z0-9\-]+)(\/{1,2})/i, (_: string, scheme: string, sep: string) => { if (sep.length === 1) { return `${scheme}:///`; // Add empty authority. } else { diff --git a/src/vs/platform/webview/common/webviewPortMapping.ts b/src/vs/platform/webview/common/webviewPortMapping.ts index e72a2986707..dbd8b65a9cd 100644 --- a/src/vs/platform/webview/common/webviewPortMapping.ts +++ b/src/vs/platform/webview/common/webviewPortMapping.ts @@ -27,7 +27,7 @@ export class WebviewPortMappingManager implements IDisposable { private readonly tunnelService: ITunnelService ) { } - public async getRedirect(resolveAuthority: IAddress, url: string): Promise { + public async getRedirect(resolveAuthority: IAddress | null | undefined, url: string): Promise { const uri = URI.parse(url); const requestLocalHostInfo = extractLocalHostUriMetaDataForPortMapping(uri); if (!requestLocalHostInfo) { @@ -38,7 +38,7 @@ export class WebviewPortMappingManager implements IDisposable { if (mapping.webviewPort === requestLocalHostInfo.port) { const extensionLocation = this._getExtensionLocation(); if (extensionLocation && extensionLocation.scheme === REMOTE_HOST_SCHEME) { - const tunnel = await this.getOrCreateTunnel(resolveAuthority, mapping.extensionHostPort); + const tunnel = resolveAuthority && await this.getOrCreateTunnel(resolveAuthority, mapping.extensionHostPort); if (tunnel) { if (tunnel.tunnelLocalPort === mapping.webviewPort) { return undefined; diff --git a/src/vs/platform/webview/electron-main/webviewPortMappingProvider.ts b/src/vs/platform/webview/electron-main/webviewPortMappingProvider.ts index 9024199c172..94dc3036ebc 100644 --- a/src/vs/platform/webview/electron-main/webviewPortMappingProvider.ts +++ b/src/vs/platform/webview/electron-main/webviewPortMappingProvider.ts @@ -36,9 +36,9 @@ export class WebviewPortMappingProvider extends Disposable { sess.webRequest.onBeforeRequest({ urls: [ - '*://localhost:*/', - '*://127.0.0.1:*/', - '*://0.0.0.0:*/', + '*://localhost:*/*', + '*://127.0.0.1:*/*', + '*://0.0.0.0:*/*', ] }, async (details, callback) => { const webviewId = details.webContentsId && this._webContentsIdsToWebviewIds.get(details.webContentsId); @@ -47,7 +47,7 @@ export class WebviewPortMappingProvider extends Disposable { } const entry = this._webviewData.get(webviewId); - if (!entry || !entry.metadata.resolvedAuthority) { + if (!entry) { return callback({}); } diff --git a/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts index 36051ae9d7f..7c158fa3895 100644 --- a/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts +++ b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts @@ -174,7 +174,7 @@ export class WebviewProtocolProvider extends Disposable { const fileService = { readFileStream: async (resource: URI): Promise => { - if (uri.scheme === Schemas.file) { + if (resource.scheme === Schemas.file) { return (await this.fileService.readFileStream(resource)).value; } diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index efd2b3b06e4..ef711dc3855 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -11521,6 +11521,141 @@ declare module 'vscode' { } //#endregion + + /** + * Represents a session of a currently logged in user. + */ + export interface AuthenticationSession { + /** + * The identifier of the authentication session. + */ + readonly id: string; + + /** + * The access token. + */ + readonly accessToken: string; + + /** + * The account associated with the session. + */ + readonly account: AuthenticationSessionAccountInformation; + + /** + * The permissions granted by the session's access token. Available scopes + * are defined by the [AuthenticationProvider](#AuthenticationProvider). + */ + readonly scopes: ReadonlyArray; + } + + /** + * The information of an account associated with an [AuthenticationSession](#AuthenticationSession). + */ + export interface AuthenticationSessionAccountInformation { + /** + * The unique identifier of the account. + */ + readonly id: string; + + /** + * The human-readable name of the account. + */ + readonly label: string; + } + + + /** + * Options to be used when getting an [AuthenticationSession](#AuthenticationSession) from an [AuthenticationProvider](#AuthenticationProvider). + */ + export interface AuthenticationGetSessionOptions { + /** + * Whether login should be performed if there is no matching session. + * + * If true, a modal dialog will be shown asking the user to sign in. If false, a numbered badge will be shown + * on the accounts activity bar icon. An entry for the extension will be added under the menu to sign in. This + * allows quietly prompting the user to sign in. + * + * Defaults to false. + */ + createIfNone?: boolean; + + /** + * Whether the existing user session preference should be cleared. + * + * For authentication providers that support being signed into multiple accounts at once, the user will be + * prompted to select an account to use when [getSession](#authentication.getSession) is called. This preference + * is remembered until [getSession](#authentication.getSession) is called with this flag. + * + * Defaults to false. + */ + clearSessionPreference?: boolean; + } + + /** + * Basic information about an [authenticationProvider](#AuthenticationProvider) + */ + export interface AuthenticationProviderInformation { + /** + * The unique identifier of the authentication provider. + */ + readonly id: string; + + /** + * The human-readable name of the authentication provider. + */ + readonly label: string; + } + + /** + * An [event](#Event) which fires when an [AuthenticationSession](#AuthenticationSession) is added, removed, or changed. + */ + export interface AuthenticationSessionsChangeEvent { + /** + * The [authenticationProvider](#AuthenticationProvider) that has had its sessions change. + */ + readonly provider: AuthenticationProviderInformation; + } + + /** + * Namespace for authentication. + */ + export namespace authentication { + /** + * Get an authentication session matching the desired scopes. Rejects if a provider with providerId is not + * registered, or if the user does not consent to sharing authentication information with + * the extension. If there are multiple sessions with the same scopes, the user will be shown a + * quickpick to select which account they would like to use. + * + * Currently, there are only two authentication providers that are contributed from built in extensions + * to VS Code that implement GitHub and Microsoft authentication: their providerId's are 'github' and 'microsoft'. + * @param providerId The id of the provider to use + * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider + * @param options The [getSessionOptions](#GetSessionOptions) to use + * @returns A thenable that resolves to an authentication session + */ + export function getSession(providerId: string, scopes: string[], options: AuthenticationGetSessionOptions & { createIfNone: true }): Thenable; + + /** + * Get an authentication session matching the desired scopes. Rejects if a provider with providerId is not + * registered, or if the user does not consent to sharing authentication information with + * the extension. If there are multiple sessions with the same scopes, the user will be shown a + * quickpick to select which account they would like to use. + * + * Currently, there are only two authentication providers that are contributed from built in extensions + * to VS Code that implement GitHub and Microsoft authentication: their providerId's are 'github' and 'microsoft'. + * @param providerId The id of the provider to use + * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider + * @param options The [getSessionOptions](#GetSessionOptions) to use + * @returns A thenable that resolves to an authentication session if available, or undefined if there are no sessions + */ + export function getSession(providerId: string, scopes: string[], options?: AuthenticationGetSessionOptions): Thenable; + + /** + * An [event](#Event) which fires when the authentication sessions of an authentication provider have + * been added, removed, or changed. + */ + export const onDidChangeSessions: Event; + } } /** diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 4b8dcf46709..e971059842b 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -18,59 +18,6 @@ declare module 'vscode' { // #region auth provider: https://github.com/microsoft/vscode/issues/88309 - export interface AuthenticationSession { - /** - * The identifier of the authentication session. - */ - readonly id: string; - - /** - * The access token. - */ - readonly accessToken: string; - - /** - * The account associated with the session. - */ - readonly account: AuthenticationSessionAccountInformation; - - /** - * The permissions granted by the session's access token. Available scopes - * are defined by the authentication provider. - */ - readonly scopes: ReadonlyArray; - } - - /** - * The information of an account associated with an authentication session. - */ - export interface AuthenticationSessionAccountInformation { - /** - * The unique identifier of the account. - */ - readonly id: string; - - /** - * The human-readable name of the account. - */ - readonly label: string; - } - - /** - * Basic information about an[authenticationProvider](#AuthenticationProvider) - */ - export interface AuthenticationProviderInformation { - /** - * The unique identifier of the authentication provider. - */ - readonly id: string; - - /** - * The human-readable name of the authentication provider. - */ - readonly label: string; - } - /** * An [event](#Event) which fires when an [AuthenticationProvider](#AuthenticationProvider) is added or removed. */ @@ -86,33 +33,10 @@ declare module 'vscode' { readonly removed: ReadonlyArray; } - /** - * Options to be used when getting a session from an [AuthenticationProvider](#AuthenticationProvider). - */ - export interface AuthenticationGetSessionOptions { - /** - * Whether login should be performed if there is no matching session. Defaults to false. - */ - createIfNone?: boolean; - - /** - * Whether the existing user session preference should be cleared. Set to allow the user to switch accounts. - * Defaults to false. - */ - clearSessionPreference?: boolean; - } - - export interface AuthenticationProviderAuthenticationSessionsChangeEvent extends AuthenticationSessionsChangeEvent { - /** - * The [authenticationProvider](#AuthenticationProvider) that has had its sessions change. - */ - readonly provider: AuthenticationProviderInformation; - } - /** * An [event](#Event) which fires when an [AuthenticationSession](#AuthenticationSession) is added, removed, or changed. */ - export interface AuthenticationSessionsChangeEvent { + export interface AuthenticationProviderAuthenticationSessionsChangeEvent { /** * The ids of the [AuthenticationSession](#AuthenticationSession)s that have been added. */ @@ -156,7 +80,7 @@ declare module 'vscode' { * An [event](#Event) which fires when the array of sessions has changed, or data * within a session has changed. */ - readonly onDidChangeSessions: Event; + readonly onDidChangeSessions: Event; /** * Returns an array of current sessions. @@ -210,30 +134,6 @@ declare module 'vscode' { */ export const providers: ReadonlyArray; - /** - * Get an authentication session matching the desired scopes. Rejects if a provider with providerId is not - * registered, or if the user does not consent to sharing authentication information with - * the extension. If there are multiple sessions with the same scopes, the user will be shown a - * quickpick to select which account they would like to use. - * @param providerId The id of the provider to use - * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider - * @param options The [getSessionOptions](#GetSessionOptions) to use - * @returns A thenable that resolves to an authentication session - */ - export function getSession(providerId: string, scopes: string[], options: AuthenticationGetSessionOptions & { createIfNone: true }): Thenable; - - /** - * Get an authentication session matching the desired scopes. Rejects if a provider with providerId is not - * registered, or if the user does not consent to sharing authentication information with - * the extension. If there are multiple sessions with the same scopes, the user will be shown a - * quickpick to select which account they would like to use. - * @param providerId The id of the provider to use - * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider - * @param options The [getSessionOptions](#GetSessionOptions) to use - * @returns A thenable that resolves to an authentication session if available, or undefined if there are no sessions - */ - export function getSession(providerId: string, scopes: string[], options: AuthenticationGetSessionOptions): Thenable; - /** * @deprecated * Logout of a specific session. @@ -242,13 +142,6 @@ declare module 'vscode' { * provider */ export function logout(providerId: string, sessionId: string): Thenable; - - /** - * An [event](#Event) which fires when the array of sessions has changed, or data - * within a session has changed for a provider. Fires with the ids of the providers - * that have had session data change. - */ - export const onDidChangeSessions: Event; } //#endregion @@ -358,13 +251,14 @@ declare module 'vscode' { export interface ResourceLabelFormatting { label: string; // myLabel:/${path} - // TODO@isidorn + // For historic reasons we use an or string here. Once we finalize this API we should start using enums instead and adopt it in extensions. // eslint-disable-next-line vscode-dts-literal-or-types separator: '/' | '\\' | ''; tildify?: boolean; normalizeDriveLetter?: boolean; workspaceSuffix?: string; authorityPrefix?: string; + stripPathStartingSeparator?: boolean; } export namespace workspace { @@ -868,30 +762,13 @@ declare module 'vscode' { debugAdapterExecutable?(folder: WorkspaceFolder | undefined, token?: CancellationToken): ProviderResult; } - export interface DebugSession { - - /** - * Terminates the session. - */ - terminate(): Thenable; - } - - export interface DebugSession { - - /** - * Terminates the session. - */ - terminate(): Thenable; - } - export namespace debug { /** - * Stop the given debug session or stop all debug sessions if no session is specified. - * @param session The [debug session](#DebugSession) to stop or `undefined` for stopping all sessions. - * @return A thenable that resolves when the sessions could be stopped successfully. + * 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 | undefined): Thenable; + export function stopDebugging(session?: DebugSession): Thenable; } //#endregion @@ -1103,8 +980,10 @@ declare module 'vscode' { * 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): ProviderResult + provideTerminalLinks(context: TerminalLinkContext, token: CancellationToken): ProviderResult /** * Handle an activated terminal link. @@ -1423,6 +1302,11 @@ declare module 'vscode' { Error = 4 } + export enum NotebookRunState { + Running = 1, + Idle = 2 + } + export interface NotebookCellMetadata { /** * Controls if the content of a cell is editable or not. @@ -1472,6 +1356,16 @@ declare module 'vscode' { */ lastRunDuration?: number; + /** + * Whether a code cell's editor is collapsed + */ + inputCollapsed?: boolean; + + /** + * Whether a code cell's outputs are collapsed + */ + outputCollapsed?: boolean; + /** * Additional attributes of a cell metadata. */ @@ -1525,6 +1419,11 @@ declare module 'vscode' { * Additional attributes of the document metadata. */ custom?: { [key: string]: any }; + + /** + * The document's current run state + */ + runState?: NotebookRunState; } export interface NotebookDocument { @@ -1532,6 +1431,7 @@ declare module 'vscode' { readonly fileName: string; readonly viewType: string; readonly isDirty: boolean; + readonly isUntitled: boolean; readonly cells: NotebookCell[]; languages: string[]; displayOrder?: GlobPattern[]; @@ -1593,6 +1493,11 @@ declare module 'vscode' { */ readonly onDidDispose: Event; + /** + * Active kernel used in the editor + */ + readonly kernel?: NotebookKernel; + /** * Fired when the output hosting webview posts a message. */ @@ -1694,6 +1599,11 @@ declare module 'vscode' { readonly language: string; } + export interface NotebookCellMetadataChangeEvent { + readonly document: NotebookDocument; + readonly cell: NotebookCell; + } + export interface NotebookCellData { readonly cellKind: CellKind; readonly source: string; @@ -1822,12 +1732,15 @@ declare module 'vscode' { } export interface NotebookKernel { + readonly id?: string; label: string; description?: string; isPreferred?: boolean; preloads?: Uri[]; - executeCell(document: NotebookDocument, cell: NotebookCell, token: CancellationToken): Promise; - executeAllCells(document: NotebookDocument, token: CancellationToken): Promise; + executeCell(document: NotebookDocument, cell: NotebookCell): void; + cancelCellExecution(document: NotebookDocument, cell: NotebookCell): void; + executeAllCells(document: NotebookDocument): void; + cancelAllCellsExecution(document: NotebookDocument): void; } export interface NotebookDocumentFilter { @@ -1867,6 +1780,7 @@ declare module 'vscode' { export const onDidOpenNotebookDocument: Event; export const onDidCloseNotebookDocument: Event; + export const onDidSaveNotebookDocument: Event; /** * All currently known notebook documents. @@ -1881,6 +1795,7 @@ declare module 'vscode' { export const onDidChangeNotebookCells: Event; export const onDidChangeCellOutputs: Event; export const onDidChangeCellLanguage: Event; + export const onDidChangeCellMetadata: Event; /** * Create a document that is the concatenation of all notebook cells. By default all code-cells are included * but a selector can be provided to narrow to down the set of cells. @@ -1890,8 +1805,7 @@ declare module 'vscode' { */ export function createConcatTextDocument(notebook: NotebookDocument, selector?: DocumentSelector): NotebookConcatTextDocument; - export let activeNotebookKernel: NotebookKernel | undefined; - export const onDidChangeActiveNotebookKernel: Event; + export const onDidChangeActiveNotebookKernel: Event<{ document: NotebookDocument, kernel: NotebookKernel | undefined }>; } //#endregion @@ -2107,6 +2021,31 @@ declare module 'vscode' { //#endregion + //#region Support `scmResourceState` in `when` clauses #86180 https://github.com/microsoft/vscode/issues/86180 + + export interface SourceControlResourceState { + /** + * Context value of the resource state. This can be used to contribute resource specific actions. + * For example, if a resource is given a context value as `diffable`. When contributing actions to `scm/resourceState/context` + * using `menus` extension point, you can specify context value for key `scmResourceState` in `when` expressions, like `scmResourceState == diffable`. + * ``` + * "contributes": { + * "menus": { + * "scm/resourceState/context": [ + * { + * "command": "extension.diff", + * "when": "scmResourceState == diffable" + * } + * ] + * } + * } + * ``` + * This will show action `extension.diff` only for resources with `contextValue` is `diffable`. + */ + readonly contextValue?: string; + } + + //#endregion //#region https://github.com/microsoft/vscode/issues/101857 export interface ExtensionContext { diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 878675c6f39..7a9e0fc6a64 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -7,7 +7,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import * as modes from 'vs/editor/common/modes'; import * as nls from 'vs/nls'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { IAuthenticationService, AllowedExtension, readAllowedExtensions } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { IAuthenticationService, AllowedExtension, readAllowedExtensions, getAuthenticationProviderActivationEvent } from 'vs/workbench/services/authentication/browser/authenticationService'; import { ExtHostAuthenticationShape, ExtHostContext, IExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; @@ -17,6 +17,8 @@ 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 { 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']; @@ -213,7 +215,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu @INotificationService private readonly notificationService: INotificationService, @IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, - @IQuickInputService private readonly quickInputService: IQuickInputService + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IExtensionService private readonly extensionService: IExtensionService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication); @@ -245,6 +248,10 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu this.authenticationService.unregisterAuthenticationProvider(id); } + $ensureProvider(id: string): Promise { + return this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(id)); + } + $sendDidChangeSessions(id: string, event: modes.AuthenticationSessionsChangeEvent): void { this.authenticationService.sessionsUpdate(id, event); } @@ -292,7 +299,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } const session = await this.authenticationService.login(providerId, scopes); - await this.$setTrustedExtension(providerId, session.account.label, extensionId, extensionName); + await this.$setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id); return session; } else { await this.$requestNewSession(providerId, scopes, extensionId, extensionName); @@ -386,7 +393,11 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } const remoteConnection = this.remoteAgentService.getConnection(); - if (remoteConnection && remoteConnection.remoteAuthority && remoteConnection.remoteAuthority.startsWith('vsonline') && VSO_ALLOWED_EXTENSIONS.includes(extensionId)) { + const isVSO = remoteConnection !== null + ? remoteConnection.remoteAuthority.startsWith('vsonline') + : platform === Platform.Web; + + if (isVSO && VSO_ALLOWED_EXTENSIONS.includes(extensionId)) { addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName); return true; } @@ -423,11 +434,13 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu return choice === 0; } - async $setTrustedExtension(providerId: string, accountName: string, extensionId: string, extensionName: string): Promise { + async $setTrustedExtensionAndAccountPreference(providerId: string, accountName: string, extensionId: string, extensionName: string, sessionId: string): Promise { const allowList = readAllowedExtensions(this.storageService, providerId, accountName); if (!allowList.find(allowed => allowed.id === extensionId)) { allowList.push({ id: extensionId, name: extensionName }); this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.GLOBAL); } + + this.storageService.store(`${extensionName}-${providerId}`, sessionId, StorageScope.GLOBAL); } } diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 368c7ee34a9..907f2886311 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -228,11 +228,13 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb public $startDebugging(folder: UriComponents | undefined, nameOrConfig: string | IDebugConfiguration, options: IStartDebuggingOptions): Promise { const folderUri = folder ? uri.revive(folder) : undefined; const launch = this.debugService.getConfigurationManager().getLaunch(folderUri); + const parentSession = this.getSession(options.parentSessionID); const debugOptions: IDebugSessionOptions = { noDebug: options.noDebug, - parentSession: this.getSession(options.parentSessionID), + parentSession, repl: options.repl, - compact: options.compact + compact: options.compact, + compoundRoot: parentSession?.compoundRoot }; return this.debugService.startDebugging(launch, nameOrConfig, debugOptions).then(success => { return success; @@ -262,14 +264,6 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return Promise.reject(new Error('debug session not found')); } - public $terminateDebugSession(sessionId: DebugSessionUUID): Promise { - const session = this.debugService.getModel().getSession(sessionId, true); - if (session) { - return session.terminate(); - } - 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/mainThreadEditor.ts b/src/vs/workbench/api/browser/mainThreadEditor.ts index 212aecd829e..f805b7f9cbe 100644 --- a/src/vs/workbench/api/browser/mainThreadEditor.ts +++ b/src/vs/workbench/api/browser/mainThreadEditor.ts @@ -269,6 +269,14 @@ export class MainThreadTextEditor { } })); + const isValidCodeEditor = () => { + // Due to event timings, it is possible that there is a model change event not yet delivered to us. + // > e.g. a model change event is emitted to a listener which then decides to update editor options + // > In this case the editor configuration change event reaches us first. + // So simply check that the model is still attached to this code editor + return (this._codeEditor && this._codeEditor.getModel() === this._model); + }; + const updateProperties = (selectionChangeSource: string | null) => { // Some editor events get delivered faster than model content changes. This is // problematic, as this leads to editor properties reaching the extension host @@ -287,18 +295,30 @@ export class MainThreadTextEditor { this._codeEditorListeners.add(this._codeEditor.onDidChangeCursorSelection((e) => { // selection + if (!isValidCodeEditor()) { + return; + } updateProperties(e.source); })); - this._codeEditorListeners.add(this._codeEditor.onDidChangeConfiguration(() => { + this._codeEditorListeners.add(this._codeEditor.onDidChangeConfiguration((e) => { // options + if (!isValidCodeEditor()) { + return; + } updateProperties(null); })); this._codeEditorListeners.add(this._codeEditor.onDidLayoutChange(() => { // visibleRanges + if (!isValidCodeEditor()) { + return; + } updateProperties(null); })); this._codeEditorListeners.add(this._codeEditor.onDidScrollChange(() => { // visibleRanges + if (!isValidCodeEditor()) { + return; + } updateProperties(null); })); this._updatePropertiesNow(null); diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index ced7611d1e4..e0b3986200d 100644 --- a/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -128,7 +128,7 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha } } - $onExtensionHostExit(code: number): void { + async $onExtensionHostExit(code: number): Promise { this._extensionService._onExtensionHostExit(code); } } diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 2b63e4e286e..9f627eb2cf4 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -3,14 +3,13 @@ * 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 { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext, INotebookDocumentsAndEditorsDelta, INotebookModelAddedData } from '../common/extHost.protocol'; -import { Disposable, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +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 { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType, CellKind, INotebookKernelInfo, INotebookKernelInfoDto, INotebookTextModelBackup, IEditor, INotebookRendererInfo, IOutputRenderRequest, IOutputRenderResponse, INotebookDocumentFilter } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +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'; @@ -19,8 +18,7 @@ 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { Emitter } from 'vs/base/common/event'; @@ -55,42 +53,8 @@ export class MainThreadNotebookDocument extends Disposable { })); } - async applyEdit(modelVersionId: number, edits: ICellEditOperation[], emitToExtHost: boolean, synchronous: boolean): Promise { - await this.notebookService.transformEditsOutputs(this.textModel, edits); - if (synchronous) { - return this._textModel.$applyEdit(modelVersionId, edits, emitToExtHost, synchronous); - } else { - return new Promise(resolve => { - this._register(DOM.scheduleAtNextAnimationFrame(() => { - const ret = this._textModel.$applyEdit(modelVersionId, edits, emitToExtHost, true); - resolve(ret); - })); - }); - } - } - - async spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]) { - await this.notebookService.transformSpliceOutputs(this.textModel, splices); - this._textModel.$spliceNotebookCellOutputs(cellHandle, splices); - } - - handleEdit(editId: number, label: string | undefined): void { - this.undoRedoService.pushElement({ - type: UndoRedoElementType.Resource, - resource: this._textModel.uri, - label: label ?? nls.localize('defaultEditLabel', "Edit"), - undo: async () => { - await this._proxy.$undoNotebook(this._textModel.viewType, this._textModel.uri, editId, this._textModel.isDirty); - }, - redo: async () => { - await this._proxy.$redoNotebook(this._textModel.viewType, this._textModel.uri, editId, this._textModel.isDirty); - }, - }); - this._textModel.setDirty(true); - } - dispose() { - this._textModel.dispose(); + // this._textModel.dispose(); super.dispose(); } } @@ -203,21 +167,21 @@ class DocumentAndEditorState { @extHostNamedCustomer(MainContext.MainThreadNotebook) export class MainThreadNotebooks extends Disposable implements MainThreadNotebookShape { - private readonly _notebookProviders = new Map(); + private readonly _notebookProviders = new Map(); private readonly _notebookKernels = new Map(); private readonly _notebookKernelProviders = new Map, provider: IDisposable }>(); private readonly _notebookRenderers = new Map(); private readonly _proxy: ExtHostNotebookShape; private _toDisposeOnEditorRemove = new Map(); private _currentState?: DocumentAndEditorState; + private _editorEventListenersMapping: 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, - @IInstantiationService private readonly _instantiationService: IInstantiationService + @IAccessibilityService private readonly accessibilityService: IAccessibilityService ) { super(); @@ -226,15 +190,23 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } async $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[], renderers: number[]): Promise { - let controller = this._notebookProviders.get(viewType); - - if (controller) { - return controller.tryApplyEdits(resource, modelVersionId, edits, renderers); + const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); + if (textModel) { + await this._notebookService.transformEditsOutputs(textModel, edits); + return textModel.$applyEdit(modelVersionId, edits, true); } return false; } + 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()); + textModelDisposableStore?.dispose(); + this._editorEventListenersMapping.delete(URI.from(uri).toString()); + } + private _isDeltaEmpty(delta: INotebookDocumentsAndEditorsDelta) { if (delta.addedDocuments !== undefined && delta.addedDocuments.length > 0) { return false; @@ -300,14 +272,43 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo this._removeNotebookEditor(editors); })); - this._register(this._notebookService.onNotebookDocumentAdd(() => { + 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); + } + }); this._updateState(); })); - this._register(this._notebookService.onNotebookDocumentRemove(() => { + this._register(this._notebookService.onNotebookDocumentRemove((documents) => { + documents.forEach(doc => { + this._editorEventListenersMapping.get(doc.toString())?.dispose(); + this._editorEventListenersMapping.delete(doc.toString()); + }); + this._updateState(); })); + this._register(this._notebookService.onDidChangeNotebookActiveKernel(e => { + this._proxy.$acceptNotebookActiveKernelChange(e); + })); + + this._register(this._notebookService.onNotebookDocumentSaved(e => { + this._proxy.$acceptModelSaved(e); + })); + const updateOrder = () => { let userOrder = this.configurationService.getValue('notebook.displayOrder'); this._proxy.$acceptDisplayOrder({ @@ -333,10 +334,6 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo this._updateState(notebookEditor); } - async addNotebookDocument(data: INotebookModelAddedData) { - this._updateState(); - } - private _addNotebookEditor(e: IEditor) { this._toDisposeOnEditorRemove.set(e.getId(), combinedDisposable( e.onDidChangeModel(() => this._updateState()), @@ -425,18 +422,92 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo this._notebookService.unregisterNotebookRenderer(id); } - async $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, supportBackup: boolean, kernel: INotebookKernelInfoDto | undefined): Promise { - let controller = new MainThreadNotebookController(this._proxy, this, viewType, supportBackup, kernel, this._notebookService, this._instantiationService); - this._notebookProviders.set(viewType, controller); - this._notebookService.registerNotebookController(viewType, extension, controller); + async $registerNotebookProvider(_extension: NotebookExtensionDescription, _viewType: string, _supportBackup: boolean, _kernel: INotebookKernelInfoDto | undefined): Promise { + const controller: IMainNotebookController = { + kernel: _kernel, + supportBackup: _supportBackup, + reloadNotebook: async (mainthreadTextModel: NotebookTextModel) => { + const data = await this._proxy.$resolveNotebookData(_viewType, mainthreadTextModel.uri); + if (!data) { + return; + } + + mainthreadTextModel.languages = data.languages; + mainthreadTextModel.metadata = data.metadata; + + const edits: ICellEditOperation[] = [ + { editType: CellEditType.Delete, count: mainthreadTextModel.cells.length, index: 0 }, + { editType: CellEditType.Insert, index: 0, cells: data.cells } + ]; + + await this._notebookService.transformEditsOutputs(mainthreadTextModel, edits); + await new Promise(resolve => { + DOM.scheduleAtNextAnimationFrame(() => { + const ret = mainthreadTextModel!.$applyEdit(mainthreadTextModel!.versionId, edits, true); + resolve(ret); + }); + }); + }, + createNotebook: async (textModel: NotebookTextModel, backupId?: string) => { + // open notebook document + const data = await this._proxy.$resolveNotebookData(textModel.viewType, textModel.uri, backupId); + if (!data) { + return; + } + + textModel.languages = data.languages; + textModel.metadata = data.metadata; + + if (data.cells.length) { + textModel.initialize(data!.cells); + } else { + 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 }); + 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); + }, + saveAs: async (uri: URI, target: URI, token: CancellationToken) => { + return this._proxy.$saveNotebookAs(_viewType, uri, target, token); + }, + backup: async (uri: URI, token: CancellationToken) => { + return this._proxy.$backup(_viewType, uri, token); + } + }; + + this._notebookProviders.set(_viewType, controller); + this._notebookService.registerNotebookController(_viewType, _extension, controller); return; } async $onNotebookChange(viewType: string, uri: UriComponents): Promise { - let controller = this._notebookProviders.get(viewType); - if (controller) { - controller.handleNotebookChange(uri); - } + const textModel = this._notebookService.getNotebookTextModel(URI.from(uri)); + textModel?.handleUnknownChange(); } async $unregisterNotebookProvider(viewType: string): Promise { @@ -462,17 +533,28 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo const emitter = new Emitter(); const that = this; const provider = this._notebookService.registerNotebookKernelProvider({ + providerExtensionId: extension.id.value, + providerDescription: extension.description, onDidChangeKernels: emitter.event, selector: documentFilter, - provideKernels: (uri: URI, token: CancellationToken) => { - return that._proxy.$provideNotebookKernels(handle, uri, token); + provideKernels: async (uri: URI, token: CancellationToken) => { + const kernels = await that._proxy.$provideNotebookKernels(handle, uri, token); + return kernels.map(kernel => { + return { + ...kernel, + providerHandle: handle + }; + }); }, resolveKernel: (editorId: string, uri: URI, kernelId: string, token: CancellationToken) => { return that._proxy.$resolveNotebookKernel(handle, editorId, uri, kernelId, token); }, - executeNotebook: (uri: URI, kernelId: string, cellHandle: number | undefined, token: CancellationToken) => { - return that._proxy.$executeNotebookKernelFromProvider(handle, uri, kernelId, cellHandle, token); - } + executeNotebook: (uri: URI, kernelId: string, cellHandle: number | undefined) => { + return that._proxy.$executeNotebookKernelFromProvider(handle, uri, kernelId, cellHandle); + }, + cancelNotebook: (uri: URI, kernelId: string, cellHandle: number | undefined) => { + return that._proxy.$cancelNotebookKernelFromProvider(handle, uri, kernelId, cellHandle); + }, }); this._notebookKernelProviders.set(handle, { extension, @@ -500,36 +582,35 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } async $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise { - let controller = this._notebookProviders.get(viewType); - - if (controller) { - controller.updateLanguages(resource, languages); - } + const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); + textModel?.updateLanguages(languages); } async $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata): Promise { - let controller = this._notebookProviders.get(viewType); - - if (controller) { - controller.updateNotebookMetadata(resource, metadata); - } + const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); + textModel?.updateNotebookMetadata(metadata); } async $updateNotebookCellMetadata(viewType: string, resource: UriComponents, handle: number, metadata: NotebookCellMetadata): Promise { - let controller = this._notebookProviders.get(viewType); - - if (controller) { - controller.updateNotebookCellMetadata(resource, handle, metadata); - } + const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); + textModel?.updateNotebookCellMetadata(handle, metadata); } async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise { - let controller = this._notebookProviders.get(viewType); - await controller?.spliceNotebookCellOutputs(resource, cellHandle, splices, renderers); + const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); + + if (textModel) { + await this._notebookService.transformSpliceOutputs(textModel, splices); + textModel.$spliceNotebookCellOutputs(cellHandle, splices); + } } - async executeNotebookByAttachedKernel(viewType: string, uri: URI, token: CancellationToken): Promise { - return this._proxy.$executeNotebookByAttachedKernel(viewType, uri, undefined, token); + 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 { @@ -543,220 +624,20 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } $onDidEdit(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void { - let controller = this._notebookProviders.get(viewType); - controller?.handleEdit(resource, editId, label); + const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); + + if (textModel) { + 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); + }); + } } $onContentChange(resource: UriComponents, viewType: string): void { - let controller = this._notebookProviders.get(viewType); - controller?.handleNotebookChange(resource); - } -} - -export class MainThreadNotebookController implements IMainNotebookController { - private _mapping: Map = new Map(); - static documentHandle: number = 0; - - constructor( - private readonly _proxy: ExtHostNotebookShape, - private _mainThreadNotebook: MainThreadNotebooks, - private _viewType: string, - private _supportBackup: boolean, - readonly kernel: INotebookKernelInfoDto | undefined, - readonly notebookService: INotebookService, - readonly _instantiationService: IInstantiationService - - ) { - } - - async createNotebook(viewType: string, uri: URI, backup: INotebookTextModelBackup | undefined, forceReload: boolean, editorId?: string, backupId?: string): Promise { - let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); - - if (mainthreadNotebook) { - if (forceReload) { - const data = await this._proxy.$resolveNotebookData(viewType, uri); - if (!data) { - return; - } - - mainthreadNotebook.textModel.languages = data.languages; - mainthreadNotebook.textModel.metadata = data.metadata; - await mainthreadNotebook.applyEdit(mainthreadNotebook.textModel.versionId, [ - { editType: CellEditType.Delete, count: mainthreadNotebook.textModel.cells.length, index: 0 }, - { editType: CellEditType.Insert, index: 0, cells: data.cells } - ], true, false); - } - return mainthreadNotebook.textModel; - } - - let document = this._instantiationService.createInstance(MainThreadNotebookDocument, this._proxy, MainThreadNotebookController.documentHandle++, viewType, this._supportBackup, uri); - this._mapping.set(document.uri.toString(), document); - - if (backup) { - // trigger events - document.textModel.metadata = backup.metadata; - document.textModel.languages = backup.languages; - - // restored from backup, update the text model without emitting any event to exthost - await document.applyEdit(document.textModel.versionId, [ - { - editType: CellEditType.Insert, - index: 0, - cells: backup.cells || [] - } - ], false, true); - - // create document in ext host with cells data - await this._mainThreadNotebook.addNotebookDocument({ - viewType: document.viewType, - handle: document.handle, - uri: document.uri, - metadata: document.textModel.metadata, - versionId: document.textModel.versionId, - cells: document.textModel.cells.map(cell => ({ - handle: cell.handle, - uri: cell.uri, - source: cell.textBuffer.getLinesContent(), - eol: cell.textBuffer.getEOL(), - language: cell.language, - cellKind: cell.cellKind, - outputs: cell.outputs, - metadata: cell.metadata - })), - attachedEditor: editorId ? { - id: editorId, - selections: document.textModel.selections - } : undefined - }); - - return document.textModel; - } - - // open notebook document - const data = await this._proxy.$resolveNotebookData(viewType, uri, backupId); - if (!data) { - return; - } - - document.textModel.languages = data.languages; - document.textModel.metadata = data.metadata; - - if (data.cells.length) { - document.textModel.initialize(data!.cells); - } else { - const mainCell = document.textModel.createCellTextModel([''], document.textModel.languages.length ? document.textModel.languages[0] : '', CellKind.Code, [], undefined); - document.textModel.insertTemplateCell(mainCell); - } - - await this._mainThreadNotebook.addNotebookDocument({ - viewType: document.viewType, - handle: document.handle, - uri: document.uri, - metadata: document.textModel.metadata, - versionId: document.textModel.versionId, - cells: document.textModel.cells.map(cell => ({ - handle: cell.handle, - uri: cell.uri, - source: cell.textBuffer.getLinesContent(), - eol: cell.textBuffer.getEOL(), - language: cell.language, - cellKind: cell.cellKind, - outputs: cell.outputs, - metadata: cell.metadata - })), - attachedEditor: editorId ? { - id: editorId, - selections: document.textModel.selections - } : undefined - }); - - this._proxy.$acceptEditorPropertiesChanged(uri, { selections: null, metadata: document.textModel.metadata }); - - return document.textModel; - } - - async resolveNotebookEditor(viewType: string, uri: URI, editorId: string) { - await this._proxy.$resolveNotebookEditor(viewType, uri, editorId); - } - - async tryApplyEdits(resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[], renderers: number[]): Promise { - let mainthreadNotebook = this._mapping.get(URI.from(resource).toString()); - - if (mainthreadNotebook) { - return await mainthreadNotebook.applyEdit(modelVersionId, edits, true, true); - } - - return false; - } - - async spliceNotebookCellOutputs(resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise { - let mainthreadNotebook = this._mapping.get(URI.from(resource).toString()); - await mainthreadNotebook?.spliceNotebookCellOutputs(cellHandle, splices); - } - - async executeNotebookByAttachedKernel(viewType: string, uri: URI, token: CancellationToken): Promise { - return this._mainThreadNotebook.executeNotebookByAttachedKernel(viewType, uri, token); - } - - onDidReceiveMessage(editorId: string, rendererType: string | undefined, message: unknown): void { - this._proxy.$onDidReceiveMessage(editorId, rendererType, message); - } - - async removeNotebookDocument(notebook: INotebookTextModel): Promise { - let document = this._mapping.get(URI.from(notebook.uri).toString()); - - if (!document) { - return; - } - - // TODO@rebornix, remove cell should use emitDelta as well to ensure document/editor events are sent together - await this._proxy.$acceptDocumentAndEditorsDelta({ removedDocuments: [notebook.uri] }); - document.dispose(); - this._mapping.delete(URI.from(notebook.uri).toString()); - } - - // Methods for ExtHost - - handleNotebookChange(resource: UriComponents) { - let document = this._mapping.get(URI.from(resource).toString()); - document?.textModel.handleUnknownChange(); - } - - handleEdit(resource: UriComponents, editId: number, label: string | undefined): void { - let document = this._mapping.get(URI.from(resource).toString()); - document?.handleEdit(editId, label); - } - - updateLanguages(resource: UriComponents, languages: string[]) { - let document = this._mapping.get(URI.from(resource).toString()); - document?.textModel.updateLanguages(languages); - } - - updateNotebookMetadata(resource: UriComponents, metadata: NotebookDocumentMetadata) { - let document = this._mapping.get(URI.from(resource).toString()); - document?.textModel.updateNotebookMetadata(metadata); - } - - updateNotebookCellMetadata(resource: UriComponents, handle: number, metadata: NotebookCellMetadata) { - let document = this._mapping.get(URI.from(resource).toString()); - document?.textModel.updateNotebookCellMetadata(handle, metadata); - } - - async executeNotebookCell(uri: URI, handle: number, token: CancellationToken): Promise { - return this._proxy.$executeNotebookByAttachedKernel(this._viewType, uri, handle, token); - } - - async save(uri: URI, token: CancellationToken): Promise { - return this._proxy.$saveNotebook(this._viewType, uri, token); - } - - async saveAs(uri: URI, target: URI, token: CancellationToken): Promise { - return this._proxy.$saveNotebookAs(this._viewType, uri, target, token); - } - - async backup(uri: URI, token: CancellationToken): Promise { - const backupId = await this._proxy.$backup(this._viewType, uri, token); - return backupId; + const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); + textModel?.handleUnknownChange(); } } @@ -772,8 +653,8 @@ export class MainThreadNotebookKernel implements INotebookKernelInfo { ) { } - async executeNotebook(viewType: string, uri: URI, handle: number | undefined, token: CancellationToken): Promise { - return this._proxy.$executeNotebook2(this.id, viewType, uri, handle, token); + async executeNotebook(viewType: string, uri: URI, handle: number | undefined): Promise { + return this._proxy.$executeNotebook2(this.id, viewType, uri, handle); } } diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 8f78a95e712..fdcce7328b5 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -66,9 +66,10 @@ class MainThreadSCMResource implements ISCMResource { private readonly sourceControlHandle: number, private readonly groupHandle: number, private readonly handle: number, - public sourceUri: URI, - public resourceGroup: ISCMResourceGroup, - public decorations: ISCMResourceDecorations + readonly sourceUri: URI, + readonly resourceGroup: ISCMResourceGroup, + readonly decorations: ISCMResourceDecorations, + readonly contextValue: string | undefined ) { } open(preserveFocus: boolean): Promise { @@ -150,18 +151,22 @@ class MainThreadSCMProvider implements ISCMProvider { } } - $registerGroup(handle: number, id: string, label: string): void { - const group = new MainThreadSCMResourceGroup( - this.handle, - handle, - this, - {}, - label, - id - ); + $registerGroups(_groups: [number /*handle*/, string /*id*/, string /*label*/, SCMGroupFeatures][]): void { + const groups = _groups.map(([handle, id, label, features]) => { + const group = new MainThreadSCMResourceGroup( + this.handle, + handle, + this, + features, + label, + id + ); - this._groupsByHandle[handle] = group; - this.groups.splice(this.groups.elements.length, 0, [group]); + this._groupsByHandle[handle] = group; + return group; + }); + + this.groups.splice(this.groups.elements.length, 0, groups); } $updateGroup(handle: number, features: SCMGroupFeatures): void { @@ -198,7 +203,7 @@ class MainThreadSCMProvider implements ISCMProvider { for (const [start, deleteCount, rawResources] of groupSlices) { const resources = rawResources.map(rawResource => { - const [handle, sourceUri, icons, tooltip, strikeThrough, faded] = rawResource; + const [handle, sourceUri, icons, tooltip, strikeThrough, faded, contextValue] = rawResource; const icon = icons[0]; const iconDark = icons[1] || icon; const decorations = { @@ -216,7 +221,8 @@ class MainThreadSCMProvider implements ISCMProvider { handle, URI.revive(sourceUri), group, - decorations + decorations, + contextValue || undefined ); }); @@ -326,7 +332,7 @@ export class MainThreadSCM implements MainThreadSCMShape { this._repositories.delete(handle); } - $registerGroup(sourceControlHandle: number, groupHandle: number, id: string, label: string): void { + $registerGroups(sourceControlHandle: number, groups: [number /*handle*/, string /*id*/, string /*label*/, SCMGroupFeatures][], splices: SCMRawResourceSplices[]): void { const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -334,7 +340,8 @@ export class MainThreadSCM implements MainThreadSCMShape { } const provider = repository.provider as MainThreadSCMProvider; - provider.$registerGroup(groupHandle, id, label); + provider.$registerGroups(groups); + provider.$spliceGroupResourceStates(splices); } $updateGroup(sourceControlHandle: number, groupHandle: number, features: SCMGroupFeatures): void { diff --git a/src/vs/workbench/api/browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts index a779de27b58..536fd02dbd3 100644 --- a/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/src/vs/workbench/api/browser/mainThreadTask.ts @@ -613,6 +613,9 @@ export class MainThreadTask implements MainThreadTaskShape { public $registerTaskSystem(key: string, info: TaskSystemInfoDTO): void { let platform: Platform.Platform; switch (info.platform) { + case 'Web': + platform = Platform.Platform.Web; + break; case 'win32': platform = Platform.Platform.Windows; break; @@ -631,7 +634,7 @@ export class MainThreadTask implements MainThreadTaskShape { return URI.parse(`${info.scheme}://${info.authority}${path}`); }, context: this._extHostContext, - resolveVariables: (workspaceFolder: IWorkspaceFolder, toResolve: ResolveSet, target: ConfigurationTarget): Promise => { + resolveVariables: (workspaceFolder: IWorkspaceFolder, toResolve: ResolveSet, target: ConfigurationTarget): Promise => { const vars: string[] = []; toResolve.variables.forEach(item => vars.push(item)); return Promise.resolve(this._proxy.$resolveVariables(workspaceFolder.uri, { process: toResolve.process, variables: vars })).then(values => { @@ -639,8 +642,12 @@ export class MainThreadTask implements MainThreadTaskShape { forEach(values.variables, (entry) => { partiallyResolvedVars.push(entry.value); }); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { this._configurationResolverService.resolveWithInteraction(workspaceFolder, partiallyResolvedVars, 'tasks', undefined, target).then(resolvedVars => { + if (!resolvedVars) { + resolve(undefined); + } + const result: ResolvedVariables = { process: undefined, variables: new Map() @@ -674,4 +681,9 @@ export class MainThreadTask implements MainThreadTaskShape { } }); } + + async $registerSupportedExecutions(custom?: boolean, shell?: boolean, process?: boolean): Promise { + return this._taskService.registerSupportedExecutions(custom, shell, process); + } + } diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index c53cedd6be0..ffd7fb0faee 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -15,6 +15,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; +import { ILogService } from 'vs/platform/log/common/log'; @extHostNamedCustomer(MainContext.MainThreadTerminalService) export class MainThreadTerminalService implements MainThreadTerminalServiceShape { @@ -40,6 +41,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IEnvironmentVariableService private readonly _environmentVariableService: IEnvironmentVariableService, + @ILogService private readonly _logService: ILogService, ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalService); this._remoteAuthority = extHostContext.remoteAuthority; @@ -217,7 +219,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape executable: terminalInstance.shellLaunchConfig.executable, args: terminalInstance.shellLaunchConfig.args, cwd: terminalInstance.shellLaunchConfig.cwd, - env: terminalInstance.shellLaunchConfig.env + env: terminalInstance.shellLaunchConfig.env, + hideFromUser: terminalInstance.shellLaunchConfig.hideFromUser }; if (terminalInstance.title) { this._proxy.$acceptTerminalOpened(terminalInstance.id, terminalInstance.title, shellLaunchConfigDto); @@ -259,6 +262,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape env: request.shellLaunchConfig.env }; + this._logService.trace('Spawning ext host process', { terminalId: proxy.terminalId, shellLaunchConfigDto, request }); this._proxy.$spawnExtHostProcess( proxy.terminalId, shellLaunchConfigDto, diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index db416f7041f..0d21c18efe8 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -5,17 +5,18 @@ import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { onUnexpectedError, isPromiseCanceledError } from 'vs/base/common/errors'; +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 { Schemas } from 'vs/base/common/network'; import { basename } from 'vs/base/common/path'; import { isWeb } from 'vs/base/common/platform'; -import { isEqual, isEqualOrParent } from 'vs/base/common/resources'; +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'; @@ -38,6 +39,7 @@ import { ICreateWebViewShowOptions, IWebviewWorkbenchService, WebviewInputOption 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 { 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'; @@ -661,10 +663,12 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod fromBackup: boolean, private readonly _editable: boolean, private readonly _getEditors: () => CustomEditorInput[], - @IWorkingCopyService workingCopyService: IWorkingCopyService, - @ILabelService private readonly _labelService: ILabelService, + @IFileDialogService private readonly _fileDialogService: IFileDialogService, @IFileService private readonly _fileService: IFileService, + @ILabelService private readonly _labelService: ILabelService, @IUndoRedoService private readonly _undoService: IUndoRedoService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, ) { super(); @@ -845,11 +849,20 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod return !!await this.saveCustomEditor(options); } - public async saveCustomEditor(_options?: ISaveOptions): Promise { + public async saveCustomEditor(options?: ISaveOptions): Promise { if (!this._editable) { return undefined; } - // TODO: handle save untitled case + + if (this._editorResource.scheme === Schemas.untitled) { + const targetUri = await this.suggestUntitledSavePath(options); + if (!targetUri) { + return undefined; + } + + await this.saveCustomEditorAs(this._editorResource, targetUri, options); + return targetUri; + } const savePromise = createCancelablePromise(token => this._proxy.$onSave(this._editorResource, this.viewType, token)); this._ongoingSave?.cancel(); @@ -871,6 +884,18 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod return this._editorResource; } + private suggestUntitledSavePath(options: ISaveOptions | undefined): Promise { + if (this._editorResource.scheme !== Schemas.untitled) { + throw new Error('Resource is not untitled'); + } + + const remoteAuthority = this._environmentService.configuration.remoteAuthority; + const localResrouce = toLocalResource(this._editorResource, remoteAuthority); + + + return this._fileDialogService.pickFileToSave(localResrouce, options?.availableFileSystems); + } + public async saveCustomEditorAs(resource: URI, targetResource: URI, _options?: ISaveOptions): Promise { if (this._editable) { // TODO: handle cancellation diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 0762abe8c4d..25dadd40aab 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -127,8 +127,9 @@ const viewDescriptor: IJSONSchema = { 'hidden', 'collapsed' ], + default: 'visible', enumDescriptions: [ - localize('vscode.extension.contributes.view.initialState.visible', "The default initial state for view. The view will be expanded. This may have different behavior when the view container that the view is in is built in."), + localize('vscode.extension.contributes.view.initialState.visible', "The default initial state for the view. In most containers the view will be expanded, however; some built-in containers (explorer, scm, and debug) show all contributed views collapsed regardless of the `visibility`."), localize('vscode.extension.contributes.view.initialState.hidden', "The view will not be shown in the view container, but will be discoverable through the views menu and other view entry points and can be un-hidden by the user."), localize('vscode.extension.contributes.view.initialState.collapsed', "The view will show in the view container, but will be collapsed.") ] diff --git a/src/vs/workbench/api/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts index 31c72b613cb..83c0ab29ec7 100644 --- a/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -8,7 +8,7 @@ import * as objects from 'vs/base/common/objects'; import { Registry } from 'vs/platform/registry/common/platform'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { IConfigurationNode, IConfigurationRegistry, Extensions, resourceLanguageSettingsSchemaId, IDefaultConfigurationExtension, validateProperty, ConfigurationScope, OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationNode, IConfigurationRegistry, Extensions, resourceLanguageSettingsSchemaId, validateProperty, ConfigurationScope, OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { workspaceSettingsSchemaId, launchSchemaId, tasksSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { isObject } from 'vs/base/common/types'; @@ -105,20 +105,11 @@ const defaultConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint { if (removed.length) { - const removedDefaultConfigurations: IDefaultConfigurationExtension[] = removed.map(extension => { - const id = extension.description.identifier; - const name = extension.description.name; - const defaults = objects.deepClone(extension.value); - return { - id, name, defaults - }; - }); + const removedDefaultConfigurations = removed.map>(extension => objects.deepClone(extension.value)); configurationRegistry.deregisterDefaultConfigurations(removedDefaultConfigurations); } if (added.length) { - const addedDefaultConfigurations = added.map(extension => { - const id = extension.description.identifier; - const name = extension.description.name; + const addedDefaultConfigurations = added.map>(extension => { const defaults: IStringDictionary = objects.deepClone(extension.value); for (const key of Object.keys(defaults)) { if (!OVERRIDE_PROPERTY_PATTERN.test(key) || typeof defaults[key] !== 'object') { @@ -126,9 +117,7 @@ defaultConfigurationExtPoint.setHandler((extensions, { added, removed }) => { delete defaults[key]; } } - return { - id, name, defaults - }; + return defaults; }); configurationRegistry.registerDefaultConfigurations(addedDefaultConfigurations); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index b343f44f2d8..97793666ad8 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -205,13 +205,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get providers(): ReadonlyArray { return extHostAuthentication.providers; }, - getSession(providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions) { + getSession(providerId: string, scopes: string[], options?: vscode.AuthenticationGetSessionOptions) { return extHostAuthentication.getSession(extension, providerId, scopes, options as any); }, logout(providerId: string, sessionId: string): Thenable { return extHostAuthentication.logout(providerId, sessionId); }, - get onDidChangeSessions(): Event { + get onDidChangeSessions(): Event { return extHostAuthentication.onDidChangeSessions; }, }; @@ -870,7 +870,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I } return extHostDebugService.startDebugging(folder, nameOrConfig, parentSessionOrOptions || {}); }, - stopDebugging(session: vscode.DebugSession | undefined) { + stopDebugging(session?: vscode.DebugSession) { return extHostDebugService.stopDebugging(session); }, addBreakpoints(breakpoints: vscode.Breakpoint[]) { @@ -921,6 +921,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.onDidCloseNotebookDocument; }, + get onDidSaveNotebookDocument(): Event { + checkProposedApiEnabled(extension); + return extHostNotebook.onDidSaveNotebookDocument; + }, get notebookDocuments(): vscode.NotebookDocument[] { checkProposedApiEnabled(extension); return extHostNotebook.notebookDocuments; @@ -933,10 +937,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeVisibleNotebookEditors; }, - get activeNotebookKernel() { - checkProposedApiEnabled(extension); - return extHostNotebook.activeNotebookKernel; - }, get onDidChangeActiveNotebookKernel() { checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeActiveNotebookKernel; @@ -977,6 +977,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeCellLanguage(listener, thisArgs, disposables); }, + onDidChangeCellMetadata(listener, thisArgs?, disposables?) { + checkProposedApiEnabled(extension); + return extHostNotebook.onDidChangeCellMetadata(listener, thisArgs, disposables); + }, createConcatTextDocument(notebook, selector) { checkProposedApiEnabled(extension); return new ExtHostNotebookConcatDocument(extHostNotebook, extHostDocuments, notebook, selector); @@ -1112,7 +1116,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TimelineItem: extHostTypes.TimelineItem, CellKind: extHostTypes.CellKind, CellOutputKind: extHostTypes.CellOutputKind, - NotebookCellRunState: extHostTypes.NotebookCellRunState + NotebookCellRunState: extHostTypes.NotebookCellRunState, + NotebookRunState: extHostTypes.NotebookRunState }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index b004b04eb5f..6caf716b6c6 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -159,13 +159,14 @@ export interface MainThreadCommentsShape extends IDisposable { export interface MainThreadAuthenticationShape extends IDisposable { $registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean): void; $unregisterAuthenticationProvider(id: string): void; + $ensureProvider(id: string): Promise; $getProviderIds(): Promise; $sendDidChangeSessions(providerId: string, event: modes.AuthenticationSessionsChangeEvent): void; $getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: { createIfNone?: boolean, clearSessionPreference?: boolean }): Promise; $selectSession(providerId: string, providerName: string, extensionId: string, extensionName: string, potentialSessions: modes.AuthenticationSession[], scopes: string[], clearSessionPreference: boolean): Promise; $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise; $loginPrompt(providerName: string, extensionName: string): Promise; - $setTrustedExtension(providerId: string, accountName: string, extensionId: string, extensionName: string): Promise; + $setTrustedExtensionAndAccountPreference(providerId: string, accountName: string, extensionId: string, extensionName: string, sessionId: string): Promise; $requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise; $getSessions(providerId: string): Promise>; @@ -597,6 +598,7 @@ export interface WebviewExtensionDescription { export interface NotebookExtensionDescription { readonly id: ExtensionIdentifier; readonly location: UriComponents; + readonly description?: string; } export enum WebviewEditorCapabilities { @@ -786,6 +788,7 @@ export interface MainThreadTaskShape extends IDisposable { $terminateTask(id: string): Promise; $registerTaskSystem(scheme: string, info: tasks.TaskSystemInfoDTO): void; $customExecutionComplete(id: string, result?: number): Promise; + $registerSupportedExecutions(custom?: boolean, shell?: boolean, process?: boolean): Promise; } export interface MainThreadExtensionServiceShape extends IDisposable { @@ -794,7 +797,7 @@ export interface MainThreadExtensionServiceShape extends IDisposable { $onDidActivateExtension(extensionId: ExtensionIdentifier, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationReason: ExtensionActivationReason): void; $onExtensionActivationError(extensionId: ExtensionIdentifier, error: ExtensionActivationError): Promise; $onExtensionRuntimeError(extensionId: ExtensionIdentifier, error: SerializedError): void; - $onExtensionHostExit(code: number): void; + $onExtensionHostExit(code: number): Promise; } export interface SCMProviderFeatures { @@ -815,7 +818,8 @@ export type SCMRawResource = [ UriComponents[] /*icons: light, dark*/, string /*tooltip*/, boolean /*strike through*/, - boolean /*faded*/ + boolean /*faded*/, + string /*context value*/ ]; export type SCMRawResourceSplice = [ @@ -834,7 +838,7 @@ export interface MainThreadSCMShape extends IDisposable { $updateSourceControl(handle: number, features: SCMProviderFeatures): void; $unregisterSourceControl(handle: number): void; - $registerGroup(sourceControlHandle: number, handle: number, id: string, label: string): void; + $registerGroups(sourceControlHandle: number, groups: [number /*handle*/, string /*id*/, string /*label*/, SCMGroupFeatures][], splices: SCMRawResourceSplices[]): void; $updateGroup(sourceControlHandle: number, handle: number, features: SCMGroupFeatures): void; $updateGroupLabel(sourceControlHandle: number, handle: number, label: string): void; $unregisterGroup(sourceControlHandle: number, handle: number): void; @@ -877,7 +881,6 @@ export interface MainThreadDebugServiceShape extends IDisposable { $stopDebugging(sessionId: DebugSessionUUID | undefined): Promise; $setDebugSessionName(id: DebugSessionUUID, name: string): void; $customDebugAdapterRequest(id: DebugSessionUUID, command: string, args: any): Promise; - $terminateDebugSession(id: DebugSessionUUID): Promise; $appendDebugConsole(value: string): void; $startBreakpointEvents(): void; $registerBreakpoints(breakpoints: Array): Promise; @@ -1382,6 +1385,7 @@ export interface IShellLaunchConfigDto { args?: string[] | string; cwd?: string | UriComponents; env?: { [key: string]: string | null; }; + hideFromUser?: boolean; } export interface IShellDefinitionDto { @@ -1616,17 +1620,21 @@ 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, token: CancellationToken): Promise; - $executeNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined, token: CancellationToken): Promise; - $executeNotebook2(kernelId: string, viewType: string, uri: UriComponents, cellHandle: number | undefined, 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; $saveNotebook(viewType: string, uri: UriComponents, token: CancellationToken): Promise; $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise; $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; + $acceptModelSaved(uriComponents: UriComponents): void; $acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void; $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): Promise; $undoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise; diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index 9ade3fff5fb..da8a6820ca0 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -21,8 +21,8 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { private _onDidChangeAuthenticationProviders = new Emitter(); readonly onDidChangeAuthenticationProviders: Event = this._onDidChangeAuthenticationProviders.event; - private _onDidChangeSessions = new Emitter(); - readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; + private _onDidChangeSessions = new Emitter(); + readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; constructor(mainContext: IMainContext) { this._proxy = mainContext.getProxy(MainContext.MainThreadAuthentication); @@ -37,11 +37,12 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { } get providers(): ReadonlyArray { - return Object.freeze(this._providers); + return Object.freeze(this._providers.slice()); } async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions & { createIfNone: true }): Promise; - async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions): Promise { + async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions = {}): Promise { + await this._proxy.$ensureProvider(providerId); const provider = this._authenticationProviders.get(providerId); const extensionName = requestingExtension.displayName || requestingExtension.name; const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); @@ -75,7 +76,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { } const session = await provider.login(scopes); - await this._proxy.$setTrustedExtension(providerId, session.account.label, extensionId, extensionName); + await this._proxy.$setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id); return session; } else { await this._proxy.$requestNewSession(providerId, scopes, extensionId, extensionName); diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index af74467148f..82121bcb2c6 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -51,7 +51,7 @@ export interface IExtHostDebugService extends ExtHostDebugServiceShape { addBreakpoints(breakpoints0: vscode.Breakpoint[]): Promise; removeBreakpoints(breakpoints0: vscode.Breakpoint[]): Promise; startDebugging(folder: vscode.WorkspaceFolder | undefined, nameOrConfig: string | vscode.DebugConfiguration, options: vscode.DebugSessionOptions): Promise; - stopDebugging(session: vscode.DebugSession | undefined): Promise; + stopDebugging(session?: vscode.DebugSession): Promise; registerDebugConfigurationProvider(type: string, provider: vscode.DebugConfigurationProvider, trigger: vscode.DebugConfigurationProviderTriggerKind): vscode.Disposable; registerDebugAdapterDescriptorFactory(extension: IExtensionDescription, type: string, factory: vscode.DebugAdapterDescriptorFactory): vscode.Disposable; registerDebugAdapterTrackerFactory(type: string, factory: vscode.DebugAdapterTrackerFactory): vscode.Disposable; @@ -302,7 +302,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E }); } - public stopDebugging(session: vscode.DebugSession | undefined): Promise { + public stopDebugging(session?: vscode.DebugSession): Promise { return this._debugServiceProxy.$stopDebugging(session ? session.id : undefined); } @@ -957,10 +957,6 @@ export class ExtHostDebugSession implements vscode.DebugSession { public customRequest(command: string, args: any): Promise { return this._debugServiceProxy.$customDebugAdapterRequest(this._id, command, args); } - - public terminate(): Promise { - return this._debugServiceProxy.$terminateDebugSession(this._id); - } } export class ExtHostDebugConsole implements vscode.DebugConsole { diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index b9b1d92ca20..34639e18b6f 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -5,6 +5,7 @@ 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'; @@ -193,8 +194,6 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } } - protected abstract _beforeAlmostReadyToRunExtensions(): Promise; - public async deactivateAll(): Promise { let allPromises: Promise[] = []; try { @@ -254,7 +253,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme if (!this._extensionPathIndex) { const tree = TernarySearchTree.forPaths(); const extensions = this._registry.getAllExtensionDescriptions().map(ext => { - if (!ext.main) { + if (!this._getEntryPoint(ext)) { return undefined; } return this._hostUtils.realpath(ext.extensionLocation.fsPath).then(value => tree.set(URI.file(value).fsPath, ext)); @@ -345,7 +344,8 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme const event = getTelemetryActivationEvent(extensionDescription, reason); type ActivatePluginClassification = {} & TelemetryActivationEventFragment; this._mainThreadTelemetryProxy.$publicLog2('activatePlugin', event); - if (!extensionDescription.main) { + const entryPoint = this._getEntryPoint(extensionDescription); + if (!entryPoint) { // Treat the extension as being empty => NOT AN ERROR CASE return Promise.resolve(new EmptyExtension(ExtensionActivationTimes.NONE)); } @@ -355,15 +355,13 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme const activationTimesBuilder = new ExtensionActivationTimesBuilder(reason.startup); return Promise.all([ - this._loadCommonJSModule(joinPath(extensionDescription.extensionLocation, extensionDescription.main), activationTimesBuilder), + this._loadCommonJSModule(joinPath(extensionDescription.extensionLocation, entryPoint), activationTimesBuilder), this._loadExtensionContext(extensionDescription) ]).then(values => { return AbstractExtHostExtensionService._callActivate(this._logService, extensionDescription.identifier, values[0], values[1], activationTimesBuilder); }); } - protected abstract _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise; - private _loadExtensionContext(extensionDescription: IExtensionDescription): Promise { const globalState = new ExtensionMemento(extensionDescription.identifier.value, true, this._storage); @@ -386,7 +384,14 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme subscriptions: [], get extensionUri() { return extensionDescription.extensionLocation; }, get extensionPath() { return extensionDescription.extensionLocation.fsPath; }, - asAbsolutePath(relativePath: string) { return path.join(extensionDescription.extensionLocation.fsPath, relativePath); }, + asAbsolutePath(relativePath: string) { + if (platform.isWeb) { + // web worker + return URI.joinPath(extensionDescription.extensionLocation, relativePath).toString(); + } else { + 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); }, @@ -557,7 +562,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } // after tests have run, we shutdown the host - this._gracefulExit(error || (typeof failures === 'number' && failures > 0) ? 1 /* ERROR */ : 0 /* OK */); + this._testRunnerExit(error || (typeof failures === 'number' && failures > 0) ? 1 /* ERROR */ : 0 /* OK */); }; const runResult = testRunner!.run(extensionTestsPath, oldTestRunnerCallback); @@ -567,11 +572,11 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme runResult .then(() => { c(); - this._gracefulExit(0); + this._testRunnerExit(0); }) .catch((err: Error) => { e(err.toString()); - this._gracefulExit(1); + this._testRunnerExit(1); }); } }); @@ -579,24 +584,20 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme // Otherwise make sure to shutdown anyway even in case of an error else { - this._gracefulExit(1 /* ERROR */); + this._testRunnerExit(1 /* ERROR */); } return Promise.reject(new Error(requireError ? requireError.toString() : nls.localize('extensionTestError', "Path {0} does not point to a valid extension test runner.", extensionTestsPath))); } - private _gracefulExit(code: number): void { - // to give the PH process a chance to flush any outstanding console - // messages to the main process, we delay the exit() by some time - setTimeout(() => { - // If extension tests are running, give the exit code to the renderer - if (this._initData.remote.isRemote && !!this._initData.environment.extensionTestsLocationURI) { - this._mainThreadExtensionsProxy.$onExtensionHostExit(code); - return; - } - + private _testRunnerExit(code: number): void { + // wait at most 5000ms for the renderer to confirm our exit request and for the renderer socket to drain + // (this is to ensure all outstanding messages reach the renderer) + const exitPromise = this._mainThreadExtensionsProxy.$onExtensionHostExit(code); + const drainPromise = this._extHostContext.drain(); + Promise.race([Promise.all([exitPromise, drainPromise]), timeout(5000)]).then(() => { this._hostUtils.exit(code); - }, 500); + }); } private _startExtensionHost(): Promise { @@ -751,6 +752,9 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._onDidChangeRemoteConnectionData.fire(); } + protected abstract _beforeAlmostReadyToRunExtensions(): Promise; + protected abstract _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined; + protected abstract _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise; public abstract async $setRemoteEnvironment(env: { [key: string]: string | null }): Promise; } diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index f973ad18c72..0f646812956 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -3,28 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; 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, 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 { 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, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice, INotebookEditorPropertiesChangeData, INotebookDocumentsAndEditorsDelta, IModelAddedData } from 'vs/workbench/api/common/extHost.protocol'; +import { CellKind, ExtHostNotebookShape, IMainContext, IModelAddedData, INotebookDocumentsAndEditorsDelta, INotebookEditorPropertiesChangeData, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { CellEditType, diff, ICellEditOperation, ICellInsertEdit, INotebookDisplayOrder, INotebookEditData, NotebookCellsChangedEvent, NotebookCellsSplice2, ICellDeleteEdit, notebookDocumentMetadataDefaults, NotebookCellsChangeType, NotebookDataDto, IOutputRenderRequest, IOutputRenderResponse, IOutputRenderResponseOutputInfo, IOutputRenderResponseCellInfo, IRawOutput, CellOutputKind, IProcessedOutput, INotebookKernelInfoDto2, IMainCellDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; -import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; -import { joinPath } from 'vs/base/common/resources'; -import { Schemas } from 'vs/base/common/network'; -import { hash } from 'vs/base/common/hash'; +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 * as vscode from 'vscode'; import { Cache } from './cache'; + interface IObservable { proxy: T; onDidChange: Event; @@ -50,6 +51,7 @@ interface INotebookEventEmitter { emitModelChange(events: vscode.NotebookCellsChangeEvent): void; emitCellOutputsChange(event: vscode.NotebookCellOutputsChangeEvent): void; emitCellLanguageChange(event: vscode.NotebookCellLanguageChangeEvent): void; + emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void; } const addIdToOutput = (output: IRawOutput, id = UUID.generateUuid()): IProcessedOutput => output.outputKind === CellOutputKind.Rich @@ -119,7 +121,7 @@ export class ExtHostCell extends Disposable implements vscode.NotebookCell { } set outputs(newOutputs: vscode.CellOutput[]) { - let rawDiffs = diff(this._outputs || [], newOutputs || [], (a) => { + const rawDiffs = diff(this._outputs || [], newOutputs || [], (a) => { return this._outputMapping.has(a); }); @@ -153,6 +155,11 @@ export class ExtHostCell extends Disposable implements vscode.NotebookCell { } 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(); const observableMetadata = getObservable(newMetadata); @@ -160,8 +167,6 @@ export class ExtHostCell extends Disposable implements vscode.NotebookCell { this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { this._updateMetadata(); })); - - this._updateMetadata(); } private _updateMetadata(): Promise { @@ -192,6 +197,10 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo this._proxy.$updateNotebookLanguages(this.viewType, this.uri, this._languages); } + get isUntitled() { + return this.uri.scheme === Schemas.untitled; + } + private _metadata: Required = notebookDocumentMetadataDefaults; private _metadataChangeListener: IDisposable; @@ -327,7 +336,7 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo get isDirty() { return false; } - accpetModelChanged(event: NotebookCellsChangedEvent): void { + acceptModelChanged(event: NotebookCellsChangedEvent): void { this._versionId = event.versionId; if (event.kind === NotebookCellsChangeType.Initialize) { this.$spliceNotebookCells(event.changes, true); @@ -341,6 +350,8 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo 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); } } @@ -353,8 +364,8 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo const addedCellDocuments: IModelAddedData[] = []; splices.reverse().forEach(splice => { - let cellDtos = splice[2]; - let newCells = cellDtos.map(cell => { + const cellDtos = splice[2]; + const newCells = cellDtos.map(cell => { const extCell = new ExtHostCell(this._proxy, this, this._documentsAndEditors, cell); @@ -366,7 +377,7 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo this._cellDisposableMapping.set(extCell.handle, new DisposableStore()); } - let store = this._cellDisposableMapping.get(extCell.handle)!; + const store = this._cellDisposableMapping.get(extCell.handle)!; store.add(extCell.onDidChangeOutputs((diffs) => { this.eventuallyUpdateCellOutputs(extCell, diffs); @@ -447,10 +458,17 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo 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[]) { - let renderers = new Set(); - let outputDtos: NotebookCellOutputsSplice[] = diffs.map(diff => { - let outputs = diff.toInsert; + const renderers = new Set(); + const outputDtos: NotebookCellOutputsSplice[] = diffs.map(diff => { + const outputs = diff.toInsert; return [diff.start, diff.deleteCount, outputs]; }); @@ -501,7 +519,7 @@ export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorCellE this._throwIfFinalized(); const sourceArr = Array.isArray(content) ? content : content.split(/\r|\n|\r\n/g); - let cell = { + const cell = { source: sourceArr, language, cellKind: type, @@ -603,6 +621,16 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook this._active = value; } + private _kernel?: vscode.NotebookKernel; + + get kernel() { + return this._kernel; + } + + set kernel(_kernel: vscode.NotebookKernel | undefined) { + throw readonly('kernel'); + } + private _onDidDispose = new Emitter(); readonly onDidDispose: Event = this._onDidDispose.event; private _onDidReceiveMessage = new Emitter(); @@ -636,7 +664,7 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook return Promise.resolve(true); } - let compressedEdits: ICellEditOperation[] = []; + const compressedEdits: ICellEditOperation[] = []; let compressedEditsIndex = -1; for (let i = 0; i < editData.edits.length; i++) { @@ -646,8 +674,8 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook continue; } - let prevIndex = compressedEditsIndex; - let prev = compressedEdits[prevIndex]; + const prevIndex = compressedEditsIndex; + const prev = compressedEdits[prevIndex]; if (prev.editType === CellEditType.Insert && editData.edits[i].editType === CellEditType.Insert) { if (prev.index === editData.edits[i].index) { @@ -678,6 +706,9 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook throw readonly('viewColumn'); } + updateActiveKernel(kernel?: vscode.NotebookKernel) { + this._kernel = kernel; + } async postMessage(message: any): Promise { return this._webComm.postMessage(message); } @@ -721,7 +752,7 @@ export class ExtHostNotebookOutputRenderer { } render(document: ExtHostNotebookDocument, output: vscode.CellDisplayOutput, outputId: string, mimeType: string): string { - let html = this.renderer.render(document, { output, outputId, mimeType }); + const html = this.renderer.render(document, { output, outputId, mimeType }); return html; } @@ -753,11 +784,18 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { const data = await this._provider.provideKernels(document, token) || []; const newMap = new Map(); + let kernel_unique_pool = 0; + const kernelIdCache = new Set(); const transformedData: INotebookKernelInfoDto2[] = data.map(kernel => { let id = this._kernelToId.get(kernel); if (id === undefined) { - id = UUID.generateUuid(); + if (kernel.id && kernelIdCache.has(kernel.id)) { + id = `${this._extension.identifier.value}_${kernel.id}_${kernel_unique_pool++}`; + } else { + id = `${this._extension.identifier.value}_${kernel.id || UUID.generateUuid()}`; + } + this._kernelToId.set(kernel, id); } @@ -784,6 +822,10 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { return transformedData; } + getKernel(kernelId: string) { + return this._idToKernel.get(kernelId); + } + async resolveNotebook(kernelId: string, document: ExtHostNotebookDocument, webview: vscode.NotebookCommunication, token: CancellationToken) { const kernel = this._idToKernel.get(kernelId); @@ -792,7 +834,7 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { } } - async executeNotebook(kernelId: string, document: ExtHostNotebookDocument, cell: ExtHostCell | undefined, token: CancellationToken) { + async executeNotebook(kernelId: string, document: ExtHostNotebookDocument, cell: ExtHostCell | undefined) { const kernel = this._idToKernel.get(kernelId); if (!kernel) { @@ -800,11 +842,35 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { } if (cell) { - return kernel.executeCell(document, cell, token); + return withToken(token => (kernel.executeCell as any)(document, cell, token)); } else { - return kernel.executeAllCells(document, token); + return withToken(token => (kernel.executeAllCells as any)(document, token)); } } + + async cancelNotebook(kernelId: string, document: ExtHostNotebookDocument, cell: ExtHostCell | undefined) { + const kernel = this._idToKernel.get(kernelId); + + if (!kernel) { + return; + } + + if (cell) { + return kernel.cancelCellExecution(document, cell); + } else { + return kernel.cancelAllCellsExecution(document); + } + } +} + +// TODO@roblou remove 'token' passed to all execute APIs once extensions are updated +async function withToken(cb: (token: CancellationToken) => any) { + const source = new CancellationTokenSource(); + try { + await cb(source.token); + } finally { + source.dispose(); + } } export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostNotebookOutputRenderingHandler { @@ -816,7 +882,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private readonly _notebookKernelProviders = new Map(); private readonly _documents = new Map(); private readonly _unInitializedDocuments = new Map(); - private readonly _editors = new Map(); + private readonly _editors = new Map(); private readonly _webviewComm = new Map(); private readonly _notebookOutputRenderers = new Map(); private readonly _renderersUsedInNotebooks = new WeakMap>(); @@ -826,6 +892,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN readonly onDidChangeCellOutputs = this._onDidChangeCellOutputs.event; private readonly _onDidChangeCellLanguage = new Emitter(); readonly onDidChangeCellLanguage = this._onDidChangeCellLanguage.event; + private readonly _onDidChangeCellMetadata = new Emitter(); + readonly onDidChangeCellMetadata = this._onDidChangeCellMetadata.event; private readonly _onDidChangeActiveNotebookEditor = new Emitter(); readonly onDidChangeActiveNotebookEditor = this._onDidChangeActiveNotebookEditor.event; @@ -849,10 +917,10 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN onDidOpenNotebookDocument: Event = this._onDidOpenNotebookDocument.event; private _onDidCloseNotebookDocument = new Emitter(); onDidCloseNotebookDocument: Event = this._onDidCloseNotebookDocument.event; + private _onDidSaveNotebookDocument = new Emitter(); + onDidSaveNotebookDocument: Event = this._onDidCloseNotebookDocument.event; visibleNotebookEditors: ExtHostNotebookEditor[] = []; - activeNotebookKernel?: vscode.NotebookKernel; - - private _onDidChangeActiveNotebookKernel = new Emitter(); + private _onDidChangeActiveNotebookKernel = new Emitter<{ document: ExtHostNotebookDocument, kernel: vscode.NotebookKernel | undefined; }>(); onDidChangeActiveNotebookKernel = this._onDidChangeActiveNotebookKernel.event; private _onDidChangeVisibleNotebookEditors = new Emitter(); onDidChangeVisibleNotebookEditors = this._onDidChangeVisibleNotebookEditors.event; @@ -872,7 +940,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const documentHandle = arg.notebookEditor?.notebookHandle; const cellHandle = arg.cell.handle; - for (let value of this._editors) { + for (const value of this._editors) { if (value[1].editor.document.handle === documentHandle) { const cell = value[1].editor.document.getCell(cellHandle); if (cell) { @@ -896,9 +964,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN throw new Error(`Notebook renderer for '${type}' already registered`); } - let extHostRenderer = new ExtHostNotebookOutputRenderer(type, filter, renderer); + const extHostRenderer = new ExtHostNotebookOutputRenderer(type, filter, renderer); this._notebookOutputRenderers.set(extHostRenderer.type, extHostRenderer); - this._proxy.$registerNotebookRenderer({ id: extension.identifier, location: extension.extensionLocation }, type, filter, renderer.preloads || []); + 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); @@ -978,8 +1046,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } findBestMatchedRenderer(mimeType: string): ExtHostNotebookOutputRenderer[] { - let matches: ExtHostNotebookOutputRenderer[] = []; - for (let renderer of this._notebookOutputRenderers) { + const matches: ExtHostNotebookOutputRenderer[] = []; + for (const renderer of this._notebookOutputRenderers) { if (renderer[1].matches(mimeType)) { matches.push(renderer[1]); } @@ -1023,7 +1091,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const supportBackup = !!provider.backupNotebook; - this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation }, 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, provider.kernel ? { id: viewType, label: provider.kernel.label, extensionLocation: extension.extensionLocation, preloads: provider.kernel.preloads } : undefined); return new extHostTypes.Disposable(() => { listener.dispose(); @@ -1036,7 +1104,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const handle = ExtHostNotebookController._notebookKernelProviderHandlePool++; const adapter = new ExtHostNotebookKernelProviderAdapter(this._proxy, handle, extension, provider); this._notebookKernelProviders.set(handle, adapter); - this._proxy.$registerNotebookKernelProvider({ id: extension.identifier, location: extension.extensionLocation }, handle, { + 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, @@ -1073,7 +1141,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN async $resolveNotebookKernel(handle: number, editorId: string, uri: UriComponents, kernelId: string, token: CancellationToken): Promise { await this._withAdapter(handle, uri, async (adapter, document) => { - let webComm = this._webviewComm.get(editorId); + const webComm = this._webviewComm.get(editorId); if (webComm) { await adapter.resolveNotebook(kernelId, document, webComm.contentProviderComm, token); @@ -1089,7 +1157,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._notebookKernels.set(id, { kernel, extension }); const transformedSelectors = selectors.map(selector => typeConverters.GlobPattern.from(selector)); - this._proxy.$registerNotebookKernel({ id: extension.identifier, location: extension.extensionLocation }, id, kernel.label, transformedSelectors, kernel.preloads || []); + 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); @@ -1119,7 +1187,10 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN }, 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); } @@ -1184,8 +1255,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } } - async $executeNotebookByAttachedKernel(viewType: string, uri: UriComponents, cellHandle: number | undefined, token: CancellationToken): Promise { - let document = this._documents.get(URI.revive(uri).toString()); + async $executeNotebookByAttachedKernel(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise { + const document = this._documents.get(URI.revive(uri).toString()); if (!document) { return; @@ -1197,46 +1268,75 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN if (provider.kernel) { if (cell) { - return provider.kernel.executeCell(document, cell, token); + return withToken(token => (provider.kernel!.executeCell as any)(document, cell, token)); } else { - return provider.kernel.executeAllCells(document, token); + return withToken(token => (provider.kernel!.executeAllCells as any)(document, token)); } } } } - async $executeNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined, token: CancellationToken): Promise { - await this._withAdapter(handle, uri, async (adapter, document) => { - let cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; + async $cancelNotebookByAttachedKernel(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise { + const document = this._documents.get(URI.revive(uri).toString()); - return adapter.executeNotebook(kernelId, document, cell, token); + 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); + } + } + } + } + + async $executeNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined): Promise { + await this._withAdapter(handle, uri, async (adapter, document) => { + const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; + + return adapter.executeNotebook(kernelId, document, cell); }); } - async $executeNotebook2(kernelId: string, viewType: string, uri: UriComponents, cellHandle: number | undefined, token: CancellationToken): Promise { - let document = this._documents.get(URI.revive(uri).toString()); + async $cancelNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined): Promise { + await this._withAdapter(handle, uri, async (adapter, document) => { + const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; + + return adapter.cancelNotebook(kernelId, document, cell); + }); + } + + async $executeNotebook2(kernelId: string, viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise { + const document = this._documents.get(URI.revive(uri).toString()); if (!document || document.viewType !== viewType) { return; } - let kernelInfo = this._notebookKernels.get(kernelId); + const kernelInfo = this._notebookKernels.get(kernelId); if (!kernelInfo) { return; } - let cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; + const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; if (cell) { - return kernelInfo.kernel.executeCell(document, cell, token); + return withToken(token => (kernelInfo!.kernel.executeCell as any)(document, cell, token)); } else { - return kernelInfo.kernel.executeAllCells(document, token); + return withToken(token => (kernelInfo!.kernel.executeAllCells as any)(document, token)); } } async $saveNotebook(viewType: string, uri: UriComponents, token: CancellationToken): Promise { - let document = this._documents.get(URI.revive(uri).toString()); + const document = this._documents.get(URI.revive(uri).toString()); if (!document) { return false; } @@ -1250,7 +1350,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } async $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise { - let document = this._documents.get(URI.revive(uri).toString()); + const document = this._documents.get(URI.revive(uri).toString()); if (!document) { return false; } @@ -1300,10 +1400,24 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._outputDisplayOrder = displayOrder; } + $acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelId: string | undefined; }) { + if (event.providerHandle !== undefined) { + 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) { + editor.editor.updateActiveKernel(kernel); + } + }); + this._onDidChangeActiveNotebookKernel.fire({ document, kernel }); + }); + } + } + // TODO: remove document - editor one on one mapping private _getEditorFromURI(uriComponents: UriComponents) { const uriStr = URI.revive(uriComponents).toString(); - let editor: { editor: ExtHostNotebookEditor } | undefined; + let editor: { editor: ExtHostNotebookEditor; } | undefined; this._editors.forEach(e => { if (e.editor.uri.toString() === uriStr) { editor = e; @@ -1321,12 +1435,20 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const document = this._documents.get(URI.revive(uriComponents).toString()); if (document) { - document.accpetModelChanged(event); + document.acceptModelChanged(event); + } + } + + public $acceptModelSaved(uriComponents: UriComponents): void { + const document = this._documents.get(URI.revive(uriComponents).toString()); + if (document) { + // this.$acceptDirtyStateChanged(uriComponents, false); + this._onDidSaveNotebookDocument.fire(document); } } $acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void { - let editor = this._getEditorFromURI(uriComponents); + const editor = this._getEditorFromURI(uriComponents); if (!editor) { return; @@ -1360,7 +1482,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._webviewComm.set(editorId, webComm); } - let editor = new ExtHostNotebookEditor( + const editor = new ExtHostNotebookEditor( document.viewType, editorId, revivedUri, @@ -1394,7 +1516,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN delta.removedDocuments.forEach((uri) => { const revivedUri = URI.revive(uri); const revivedUriStr = revivedUri.toString(); - let document = this._documents.get(revivedUriStr); + const document = this._documents.get(revivedUriStr); if (document) { @@ -1440,7 +1562,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN if (!this._documents.has(revivedUriStr)) { const that = this; - let document = this._unInitializedDocuments.get(revivedUriStr) ?? new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, { + const document = this._unInitializedDocuments.get(revivedUriStr) ?? new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, { emitModelChange(event: vscode.NotebookCellsChangeEvent): void { that._onDidChangeNotebookCells.fire(event); }, @@ -1449,6 +1571,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN }, emitCellLanguageChange(event: vscode.NotebookCellLanguageChangeEvent): void { that._onDidChangeCellLanguage.fire(event); + }, + emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void { + that._onDidChangeCellMetadata.fire(event); } }, viewType, revivedUri, this, storageRoot); @@ -1460,7 +1585,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN }; } - document.accpetModelChanged({ + document.acceptModelChanged({ kind: NotebookCellsChangeType.Initialize, versionId: modelData.versionId, changes: [[ @@ -1508,7 +1633,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN }); } - const removedEditors: { editor: ExtHostNotebookEditor }[] = []; + const removedEditors: { editor: ExtHostNotebookEditor; }[] = []; if (delta.removedEditors) { delta.removedEditors.forEach(editorid => { diff --git a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts index f499b9ac791..8ffeda97be3 100644 --- a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts @@ -41,7 +41,7 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD this._init(); this._disposables.add(extHostDocuments.onDidChangeDocument(e => { - let cellIdx = this._cellUris.get(e.document.uri); + const cellIdx = this._cellUris.get(e.document.uri); if (cellIdx !== undefined) { this._cellLengths.changeValue(cellIdx, this._cells[cellIdx].document.getText().length + 1); this._cellLines.changeValue(cellIdx, this._cells[cellIdx].document.lineCount); @@ -75,7 +75,7 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD this._cellUris = new ResourceMap(); const cellLengths: number[] = []; const cellLineCounts: number[] = []; - for (let cell of this._notebook.cells) { + 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); @@ -94,7 +94,7 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD getText(range?: vscode.Range): string { if (!range) { let result = ''; - for (let cell of this._cells) { + for (const cell of this._cells) { result += cell.document.getText() + '\n'; } // remove last newline again @@ -117,8 +117,8 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD } else if (startCell === endCell) { return startCell.document.getText(new types.Range(start.range.start, end.range.end)); } else { - let a = startCell.document.getText(new types.Range(start.range.start, new types.Position(startCell.document.lineCount, 0))); - let b = endCell.document.getText(new types.Range(new types.Position(0, 0), end.range.end)); + const a = startCell.document.getText(new types.Range(start.range.start, new types.Position(startCell.document.lineCount, 0))); + const b = endCell.document.getText(new types.Range(new types.Position(0, 0), end.range.end)); return a + '\n' + b; } } @@ -139,7 +139,7 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD const idx = this._cellUris.get(locationOrOffset.uri); if (idx !== undefined) { - let line = this._cellLines.getAccumulatedValue(idx - 1); + const line = this._cellLines.getAccumulatedValue(idx - 1); return new types.Position(line + locationOrOffset.range.start.line, locationOrOffset.range.start.character); } // do better? @@ -158,9 +158,9 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD endIdx = this._cellLines.getIndexOf(positionOrRange.end.line); } - let startPos = new types.Position(startIdx.remainder, positionOrRange.start.character); - let endPos = new types.Position(endIdx.remainder, positionOrRange.end.character); - let range = new types.Range(startPos, endPos); + const startPos = new types.Position(startIdx.remainder, positionOrRange.start.character); + const endPos = new types.Position(endIdx.remainder, positionOrRange.end.character); + const range = new types.Range(startPos, endPos); const startCell = this._cells[startIdx.index]; return new types.Location(startCell.uri, startCell.document.validateRange(range)); diff --git a/src/vs/workbench/api/common/extHostRpcService.ts b/src/vs/workbench/api/common/extHostRpcService.ts index 58237cf24bc..6582ef5fb3f 100644 --- a/src/vs/workbench/api/common/extHostRpcService.ts +++ b/src/vs/workbench/api/common/extHostRpcService.ts @@ -18,12 +18,12 @@ export class ExtHostRpcService implements IExtHostRpcService { readonly getProxy: (identifier: ProxyIdentifier) => T; readonly set: (identifier: ProxyIdentifier, instance: R) => R; readonly assertRegistered: (identifiers: ProxyIdentifier[]) => void; + readonly drain: () => Promise; constructor(rpcProtocol: IRPCProtocol) { this.getProxy = rpcProtocol.getProxy.bind(rpcProtocol); this.set = rpcProtocol.set.bind(rpcProtocol); this.assertRegistered = rpcProtocol.assertRegistered.bind(rpcProtocol); - + this.drain = rpcProtocol.drain.bind(rpcProtocol); } - } diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 6464c9af686..77d142d2bd6 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -6,10 +6,10 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { debounce } from 'vs/base/common/decorators'; -import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { asPromise } from 'vs/base/common/async'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; -import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape } from './extHost.protocol'; +import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures } from './extHost.protocol'; import { sortedDiff, equals } from 'vs/base/common/arrays'; import { comparePaths } from 'vs/base/common/comparers'; import type * as vscode from 'vscode'; @@ -228,6 +228,9 @@ class ExtHostSourceControlResourceGroup implements vscode.SourceControlResourceG private readonly _onDidUpdateResourceStates = new Emitter(); readonly onDidUpdateResourceStates = this._onDidUpdateResourceStates.event; + + private _disposed = false; + get disposed(): boolean { return this._disposed; } private readonly _onDidDispose = new Emitter(); readonly onDidDispose = this._onDidDispose.event; @@ -246,7 +249,13 @@ class ExtHostSourceControlResourceGroup implements vscode.SourceControlResourceG get hideWhenEmpty(): boolean | undefined { return this._hideWhenEmpty; } set hideWhenEmpty(hideWhenEmpty: boolean | undefined) { this._hideWhenEmpty = hideWhenEmpty; - this._proxy.$updateGroup(this._sourceControlHandle, this.handle, { hideWhenEmpty }); + this._proxy.$updateGroup(this._sourceControlHandle, this.handle, this.features); + } + + get features(): SCMGroupFeatures { + return { + hideWhenEmpty: this.hideWhenEmpty + }; } get resourceStates(): vscode.SourceControlResourceState[] { return [...this._resourceStates]; } @@ -263,9 +272,7 @@ class ExtHostSourceControlResourceGroup implements vscode.SourceControlResourceG private _sourceControlHandle: number, private _id: string, private _label: string, - ) { - this._proxy.$registerGroup(_sourceControlHandle, this.handle, _id, _label); - } + ) { } getResourceState(handle: number): vscode.SourceControlResourceState | undefined { return this._resourceStatesMap.get(handle); @@ -311,8 +318,9 @@ class ExtHostSourceControlResourceGroup implements vscode.SourceControlResourceG const tooltip = (r.decorations && r.decorations.tooltip) || ''; const strikeThrough = r.decorations && !!r.decorations.strikeThrough; const faded = r.decorations && !!r.decorations.faded; + const contextValue = r.contextValue || ''; - const rawResource = [handle, sourceUri, icons, tooltip, strikeThrough, faded] as SCMRawResource; + const rawResource = [handle, sourceUri, icons, tooltip, strikeThrough, faded, contextValue] as SCMRawResource; return { rawResource, handle }; }); @@ -340,7 +348,7 @@ class ExtHostSourceControlResourceGroup implements vscode.SourceControlResourceG } dispose(): void { - this._proxy.$unregisterGroup(this._sourceControlHandle, this.handle); + this._disposed = true; this._onDidDispose.fire(); } } @@ -465,26 +473,51 @@ class ExtHostSourceControl implements vscode.SourceControl { this._proxy.$registerSourceControl(this.handle, _id, _label, _rootUri); } + private createdResourceGroups = new Map(); private updatedResourceGroups = new Set(); createResourceGroup(id: string, label: string): ExtHostSourceControlResourceGroup { const group = new ExtHostSourceControlResourceGroup(this._proxy, this._commands, this.handle, id, label); - - const updateListener = group.onDidUpdateResourceStates(() => { - this.updatedResourceGroups.add(group); - this.eventuallyUpdateResourceStates(); - }); - - Event.once(group.onDidDispose)(() => { - this.updatedResourceGroups.delete(group); - updateListener.dispose(); - this._groups.delete(group.handle); - }); - - this._groups.set(group.handle, group); + const disposable = Event.once(group.onDidDispose)(() => this.createdResourceGroups.delete(group)); + this.createdResourceGroups.set(group, disposable); + this.eventuallyAddResourceGroups(); return group; } + @debounce(100) + eventuallyAddResourceGroups(): void { + const groups: [number /*handle*/, string /*id*/, string /*label*/, SCMGroupFeatures][] = []; + const splices: SCMRawResourceSplices[] = []; + + for (const [group, disposable] of this.createdResourceGroups) { + disposable.dispose(); + + const updateListener = group.onDidUpdateResourceStates(() => { + this.updatedResourceGroups.add(group); + this.eventuallyUpdateResourceStates(); + }); + + Event.once(group.onDidDispose)(() => { + this.updatedResourceGroups.delete(group); + updateListener.dispose(); + this._groups.delete(group.handle); + this._proxy.$unregisterGroup(this.handle, group.handle); + }); + + groups.push([group.handle, group.id, group.label, group.features]); + + const snapshot = group._takeResourceStateSnapshot(); + + if (snapshot.length > 0) { + splices.push([group.handle, snapshot]); + } + + this._groups.set(group.handle, group); + } + + this._proxy.$registerGroups(this.handle, groups, splices); + } + @debounce(100) eventuallyUpdateResourceStates(): void { const splices: SCMRawResourceSplices[] = []; diff --git a/src/vs/workbench/api/common/extHostTask.ts b/src/vs/workbench/api/common/extHostTask.ts index 066e6ddb489..ee6423408fd 100644 --- a/src/vs/workbench/api/common/extHostTask.ts +++ b/src/vs/workbench/api/common/extHostTask.ts @@ -419,6 +419,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask this._activeCustomExecutions2 = new Map(); this._logService = logService; this._deprecationService = deprecationService; + this._proxy.$registerSupportedExecutions(true); } public registerTaskProvider(extension: IExtensionDescription, type: string, provider: vscode.TaskProvider): vscode.Disposable { @@ -696,13 +697,11 @@ export class WorkerExtHostTask extends ExtHostTaskBase { @IExtHostApiDeprecationService deprecationService: IExtHostApiDeprecationService ) { super(extHostRpc, initData, workspaceService, editorService, configurationService, extHostTerminalService, logService, deprecationService); - if (initData.remote.isRemote && initData.remote.authority) { - this.registerTaskSystem(Schemas.vscodeRemote, { - scheme: Schemas.vscodeRemote, - authority: initData.remote.authority, - platform: Platform.PlatformToString(Platform.Platform.Web) - }); - } + this.registerTaskSystem(Schemas.vscodeRemote, { + scheme: Schemas.vscodeRemote, + authority: '', + platform: Platform.PlatformToString(Platform.Platform.Web) + }); } public async executeTask(extension: IExtensionDescription, task: vscode.Task): Promise { diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index e01ab6a1513..ed8fd570cdd 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -20,6 +20,7 @@ import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib import { localize } from 'vs/nls'; import { NotSupportedError } from 'vs/base/common/errors'; import { serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; export interface IExtHostTerminalService extends ExtHostTerminalServiceShape { @@ -320,6 +321,7 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ private readonly _linkHandlers: Set = new Set(); private readonly _linkProviders: Set = new Set(); private readonly _terminalLinkCache: Map> = new Map(); + private readonly _terminalLinkCancellationSource: Map = new Map(); public get activeTerminal(): ExtHostTerminal | undefined { return this._activeTerminal; } public get terminals(): ExtHostTerminal[] { return this._terminals; } @@ -455,7 +457,8 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ shellPath: shellLaunchConfigDto.executable, shellArgs: shellLaunchConfigDto.args, cwd: typeof shellLaunchConfigDto.cwd === 'string' ? shellLaunchConfigDto.cwd : URI.revive(shellLaunchConfigDto.cwd), - env: shellLaunchConfigDto.env + env: shellLaunchConfigDto.env, + hideFromUser: shellLaunchConfigDto.hideFromUser }; const terminal = new ExtHostTerminal(this._proxy, creationOptions, name, id); this._terminals.push(terminal); @@ -611,17 +614,33 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ // when new links are provided. this._terminalLinkCache.delete(terminalId); + const oldToken = this._terminalLinkCancellationSource.get(terminalId); + if (oldToken) { + oldToken.dispose(true); + } + const cancellationSource = new CancellationTokenSource(); + this._terminalLinkCancellationSource.set(terminalId, cancellationSource); + const result: ITerminalLinkDto[] = []; const context: vscode.TerminalLinkContext = { terminal, line }; const promises: vscode.ProviderResult<{ provider: vscode.TerminalLinkProvider, links: vscode.TerminalLink[] }>[] = []; + for (const provider of this._linkProviders) { promises.push(new Promise(async r => { - const links = (await provider.provideTerminalLinks(context)) || []; - r({ provider, links }); + cancellationSource.token.onCancellationRequested(() => r({ provider, links: [] })); + const links = (await provider.provideTerminalLinks(context, cancellationSource.token)) || []; + if (!cancellationSource.token.isCancellationRequested) { + r({ provider, links }); + } })); } const provideResults = await Promise.all(promises); + + if (cancellationSource.token.isCancellationRequested) { + return []; + } + const cacheLinkMap = new Map(); for (const provideResult of provideResults) { if (provideResult && provideResult.links.length > 0) { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 1de7259f01b..447fa8092c6 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2739,6 +2739,11 @@ export enum NotebookCellRunState { Error = 4 } +export enum NotebookRunState { + Running = 1, + Idle = 2 +} + //#endregion //#region Timeline diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index 9e38664bafc..25e3d1e692d 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -10,14 +10,166 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { forEach } from 'vs/base/common/collections'; import { IExtensionPointUser, ExtensionMessageCollector, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { MenuId, MenuRegistry, ILocalizedString, IMenuItem, ICommandAction } from 'vs/platform/actions/common/actions'; +import { MenuId, MenuRegistry, ILocalizedString, IMenuItem, ICommandAction, ISubmenuItem } from 'vs/platform/actions/common/actions'; import { URI } from 'vs/base/common/uri'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { Iterable } from 'vs/base/common/iterator'; +import { index } from 'vs/base/common/arrays'; + +interface IAPIMenu { + readonly key: string; + readonly id: MenuId; + readonly description: string; + readonly proposed?: boolean; // defaults to false + readonly supportsSubmenus?: boolean; // defaults to true +} + +const apiMenus: IAPIMenu[] = [ + { + key: 'commandPalette', + id: MenuId.CommandPalette, + description: localize('menus.commandPalette', "The Command Palette"), + supportsSubmenus: false + }, + { + key: 'touchBar', + id: MenuId.TouchBarContext, + description: localize('menus.touchBar', "The touch bar (macOS only)"), + supportsSubmenus: false + }, + { + key: 'editor/title', + id: MenuId.EditorTitle, + description: localize('menus.editorTitle', "The editor title menu") + }, + { + key: 'editor/context', + id: MenuId.EditorContext, + description: localize('menus.editorContext', "The editor context menu") + }, + { + key: 'explorer/context', + id: MenuId.ExplorerContext, + description: localize('menus.explorerContext', "The file explorer context menu") + }, + { + key: 'editor/title/context', + id: MenuId.EditorTitleContext, + description: localize('menus.editorTabContext', "The editor tabs context menu") + }, + { + key: 'debug/callstack/context', + id: MenuId.DebugCallStackContext, + description: localize('menus.debugCallstackContext', "The debug callstack context menu") + }, + { + key: 'debug/toolBar', + id: MenuId.DebugToolBar, + description: localize('menus.debugToolBar', "The debug toolbar menu") + }, + { + key: 'menuBar/webNavigation', + id: MenuId.MenubarWebNavigationMenu, + description: localize('menus.webNavigation', "The top level navigational menu (web only)"), + proposed: true, + supportsSubmenus: false + }, + { + key: 'scm/title', + id: MenuId.SCMTitle, + description: localize('menus.scmTitle', "The Source Control title menu") + }, + { + key: 'scm/sourceControl', + id: MenuId.SCMSourceControl, + description: localize('menus.scmSourceControl', "The Source Control menu") + }, + { + key: 'scm/resourceState/context', + id: MenuId.SCMResourceContext, + description: localize('menus.resourceGroupContext', "The Source Control resource group context menu") + }, + { + key: 'scm/resourceFolder/context', + id: MenuId.SCMResourceFolderContext, + description: localize('menus.resourceStateContext', "The Source Control resource state context menu") + }, + { + key: 'scm/resourceGroup/context', + id: MenuId.SCMResourceGroupContext, + description: localize('menus.resourceFolderContext', "The Source Control resource folder context menu") + }, + { + key: 'scm/change/title', + id: MenuId.SCMChangeContext, + description: localize('menus.changeTitle', "The Source Control inline change menu") + }, + { + key: 'statusBar/windowIndicator', + id: MenuId.StatusBarWindowIndicatorMenu, + description: localize('menus.statusBarWindowIndicator', "The window indicator menu in the status bar"), + proposed: true, + supportsSubmenus: false + }, + { + key: 'view/title', + id: MenuId.ViewTitle, + description: localize('view.viewTitle', "The contributed view title menu") + }, + { + key: 'view/item/context', + id: MenuId.ViewItemContext, + description: localize('view.itemContext', "The contributed view item context menu") + }, + { + key: 'comments/commentThread/title', + id: MenuId.CommentThreadTitle, + description: localize('commentThread.title', "The contributed comment thread title menu") + }, + { + key: 'comments/commentThread/context', + id: MenuId.CommentThreadActions, + description: localize('commentThread.actions', "The contributed comment thread context menu, rendered as buttons below the comment editor"), + supportsSubmenus: false + }, + { + key: 'comments/comment/title', + id: MenuId.CommentTitle, + description: localize('comment.title', "The contributed comment title menu") + }, + { + key: 'comments/comment/context', + id: MenuId.CommentActions, + description: localize('comment.actions', "The contributed comment context menu, rendered as buttons below the comment editor"), + supportsSubmenus: false + }, + { + key: 'notebook/cell/title', + id: MenuId.NotebookCellTitle, + description: localize('notebook.cell.title', "The contributed notebook cell title menu"), + proposed: true + }, + { + key: 'extension/context', + id: MenuId.ExtensionContext, + description: localize('menus.extensionContext', "The extension context menu") + }, + { + key: 'timeline/title', + id: MenuId.TimelineTitle, + description: localize('view.timelineTitle', "The Timeline view title menu") + }, + { + key: 'timeline/item/context', + id: MenuId.TimelineItemContext, + description: localize('view.timelineContext', "The Timeline view item context menu") + }, +]; namespace schema { - // --- menus contribution point + // --- menus, submenus contribution point export interface IUserFriendlyMenuItem { command: string; @@ -26,80 +178,102 @@ namespace schema { group?: string; } - export function parseMenuId(value: string): MenuId | undefined { - switch (value) { - case 'commandPalette': return MenuId.CommandPalette; - case 'touchBar': return MenuId.TouchBarContext; - case 'editor/title': return MenuId.EditorTitle; - case 'editor/context': return MenuId.EditorContext; - case 'explorer/context': return MenuId.ExplorerContext; - case 'editor/title/context': return MenuId.EditorTitleContext; - case 'debug/callstack/context': return MenuId.DebugCallStackContext; - case 'debug/toolbar': return MenuId.DebugToolBar; - case 'debug/toolBar': return MenuId.DebugToolBar; - case 'menuBar/webNavigation': return MenuId.MenubarWebNavigationMenu; - case 'scm/title': return MenuId.SCMTitle; - case 'scm/sourceControl': return MenuId.SCMSourceControl; - case 'scm/resourceState/context': return MenuId.SCMResourceContext;// - case 'scm/resourceFolder/context': return MenuId.SCMResourceFolderContext; - case 'scm/resourceGroup/context': return MenuId.SCMResourceGroupContext; - case 'scm/change/title': return MenuId.SCMChangeContext;// - case 'statusBar/windowIndicator': return MenuId.StatusBarWindowIndicatorMenu; - case 'view/title': return MenuId.ViewTitle; - case 'view/item/context': return MenuId.ViewItemContext; - case 'comments/commentThread/title': return MenuId.CommentThreadTitle; - case 'comments/commentThread/context': return MenuId.CommentThreadActions; - case 'comments/comment/title': return MenuId.CommentTitle; - case 'comments/comment/context': return MenuId.CommentActions; - case 'notebook/cell/title': return MenuId.NotebookCellTitle; - case 'extension/context': return MenuId.ExtensionContext; - case 'timeline/title': return MenuId.TimelineTitle; - case 'timeline/item/context': return MenuId.TimelineItemContext; - } - - return undefined; + export interface IUserFriendlySubmenuItem { + submenu: string; + when?: string; + group?: string; } - export function isProposedAPI(menuId: MenuId): boolean { - switch (menuId) { - case MenuId.StatusBarWindowIndicatorMenu: - case MenuId.MenubarWebNavigationMenu: - case MenuId.NotebookCellTitle: - return true; - } - return false; + export interface IUserFriendlySubmenu { + id: string; + label: string; + icon?: IUserFriendlyIcon; } - export function isValidMenuItems(menu: IUserFriendlyMenuItem[], collector: ExtensionMessageCollector): boolean { - if (!Array.isArray(menu)) { - collector.error(localize('requirearray', "menu items must be an array")); + export function isMenuItem(item: IUserFriendlyMenuItem | IUserFriendlySubmenuItem): item is IUserFriendlyMenuItem { + return typeof (item as IUserFriendlyMenuItem).command === 'string'; + } + + export function isValidMenuItem(item: IUserFriendlyMenuItem, collector: ExtensionMessageCollector): boolean { + if (typeof item.command !== 'string') { + collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'command')); + return false; + } + if (item.alt && typeof item.alt !== 'string') { + collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'alt')); + return false; + } + if (item.when && typeof item.when !== 'string') { + collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'when')); + return false; + } + if (item.group && typeof item.group !== 'string') { + collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'group')); return false; } - for (let item of menu) { - if (typeof item.command !== 'string') { - collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'command')); - return false; - } - if (item.alt && typeof item.alt !== 'string') { - collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'alt')); - return false; - } - if (item.when && typeof item.when !== 'string') { - collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'when')); - return false; - } - if (item.group && typeof item.group !== 'string') { - collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'group')); - return false; + return true; + } + + export function isValidSubmenuItem(item: IUserFriendlySubmenuItem, collector: ExtensionMessageCollector): boolean { + if (typeof item.submenu !== 'string') { + collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'submenu')); + return false; + } + if (item.when && typeof item.when !== 'string') { + collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'when')); + return false; + } + if (item.group && typeof item.group !== 'string') { + collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'group')); + return false; + } + + return true; + } + + export function isValidItems(items: (IUserFriendlyMenuItem | IUserFriendlySubmenuItem)[], collector: ExtensionMessageCollector): boolean { + if (!Array.isArray(items)) { + collector.error(localize('requirearray', "submenu items must be an array")); + return false; + } + + for (let item of items) { + if (isMenuItem(item)) { + if (!isValidMenuItem(item, collector)) { + return false; + } + } else { + if (!isValidSubmenuItem(item, collector)) { + return false; + } } } return true; } + export function isValidSubmenu(submenu: IUserFriendlySubmenu, collector: ExtensionMessageCollector): boolean { + if (typeof submenu !== 'object') { + collector.error(localize('require', "submenu items must be an object")); + return false; + } + + if (typeof submenu.id !== 'string') { + collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'id')); + return false; + } + if (typeof submenu.label !== 'string') { + collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'label')); + return false; + } + + return true; + } + const menuItem: IJSONSchema = { type: 'object', + required: ['command'], properties: { command: { description: localize('vscode.extension.contributes.menuItem.command', 'Identifier of the command to execute. The command must be declared in the \'commands\'-section'), @@ -114,144 +288,86 @@ namespace schema { type: 'string' }, group: { - description: localize('vscode.extension.contributes.menuItem.group', 'Group into which this command belongs'), + description: localize('vscode.extension.contributes.menuItem.group', 'Group into which this item belongs'), type: 'string' } } }; + const submenuItem: IJSONSchema = { + type: 'object', + required: ['submenu'], + properties: { + submenu: { + description: localize('vscode.extension.contributes.menuItem.submenu', 'Identifier of the submenu to display in this item.'), + type: 'string' + }, + when: { + description: localize('vscode.extension.contributes.menuItem.when', 'Condition which must be true to show this item'), + type: 'string' + }, + group: { + description: localize('vscode.extension.contributes.menuItem.group', 'Group into which this item belongs'), + type: 'string' + } + } + }; + + const submenu: IJSONSchema = { + type: 'object', + required: ['id', 'label'], + properties: { + id: { + description: localize('vscode.extension.contributes.submenu.id', 'Identifier of the menu to display as a submenu.'), + type: 'string' + }, + label: { + description: localize('vscode.extension.contributes.submenu.label', 'The label of the menu item which leads to this submenu.'), + type: 'string' + }, + icon: { + description: localize('vscode.extension.contributes.submenu.icon', '(Optional) Icon which is used to represent the submenu in the UI. Either a file path, an object with file paths for dark and light themes, or a theme icon references, like `\\$(zap)`'), + anyOf: [{ + type: 'string' + }, + { + type: 'object', + properties: { + light: { + description: localize('vscode.extension.contributes.submenu.icon.light', 'Icon path when a light theme is used'), + type: 'string' + }, + dark: { + description: localize('vscode.extension.contributes.submenu.icon.dark', 'Icon path when a dark theme is used'), + type: 'string' + } + } + }] + } + } + }; + export const menusContribution: IJSONSchema = { description: localize('vscode.extension.contributes.menus', "Contributes menu items to the editor"), type: 'object', - properties: { - 'commandPalette': { - description: localize('menus.commandPalette', "The Command Palette"), - type: 'array', - items: menuItem - }, - 'touchBar': { - description: localize('menus.touchBar', "The touch bar (macOS only)"), - type: 'array', - items: menuItem - }, - 'editor/title': { - description: localize('menus.editorTitle', "The editor title menu"), - type: 'array', - items: menuItem - }, - 'editor/context': { - description: localize('menus.editorContext', "The editor context menu"), - type: 'array', - items: menuItem - }, - 'explorer/context': { - description: localize('menus.explorerContext', "The file explorer context menu"), - type: 'array', - items: menuItem - }, - 'editor/title/context': { - description: localize('menus.editorTabContext', "The editor tabs context menu"), - type: 'array', - items: menuItem - }, - 'debug/callstack/context': { - description: localize('menus.debugCallstackContext', "The debug callstack context menu"), - type: 'array', - items: menuItem - }, - 'debug/toolBar': { - description: localize('menus.debugToolBar', "The debug toolbar menu"), - type: 'array', - items: menuItem - }, - 'menuBar/webNavigation': { - description: localize('menus.webNavigation', "The top level navigational menu (web only)"), - type: 'array', - items: menuItem - }, - 'scm/title': { - description: localize('menus.scmTitle', "The Source Control title menu"), - type: 'array', - items: menuItem - }, - 'scm/sourceControl': { - description: localize('menus.scmSourceControl', "The Source Control menu"), - type: 'array', - items: menuItem - }, - 'scm/resourceGroup/context': { - description: localize('menus.resourceGroupContext', "The Source Control resource group context menu"), - type: 'array', - items: menuItem - }, - 'scm/resourceState/context': { - description: localize('menus.resourceStateContext', "The Source Control resource state context menu"), - type: 'array', - items: menuItem - }, - 'scm/resourceFolder/context': { - description: localize('menus.resourceFolderContext', "The Source Control resource folder context menu"), - type: 'array', - items: menuItem - }, - 'scm/change/title': { - description: localize('menus.changeTitle', "The Source Control inline change menu"), - type: 'array', - items: menuItem - }, - 'view/title': { - description: localize('view.viewTitle', "The contributed view title menu"), - type: 'array', - items: menuItem - }, - 'view/item/context': { - description: localize('view.itemContext', "The contributed view item context menu"), - type: 'array', - items: menuItem - }, - 'comments/commentThread/title': { - description: localize('commentThread.title', "The contributed comment thread title menu"), - type: 'array', - items: menuItem - }, - 'comments/commentThread/context': { - description: localize('commentThread.actions', "The contributed comment thread context menu, rendered as buttons below the comment editor"), - type: 'array', - items: menuItem - }, - 'comments/comment/title': { - description: localize('comment.title', "The contributed comment title menu"), - type: 'array', - items: menuItem - }, - 'comments/comment/context': { - description: localize('comment.actions', "The contributed comment context menu, rendered as buttons below the comment editor"), - type: 'array', - items: menuItem - }, - 'notebook/cell/title': { - description: localize('notebook.cell.title', "The contributed notebook cell title menu"), - type: 'array', - items: menuItem - }, - 'extension/context': { - description: localize('menus.extensionContext', "The extension context menu"), - type: 'array', - items: menuItem - }, - 'timeline/title': { - description: localize('view.timelineTitle', "The Timeline view title menu"), - type: 'array', - items: menuItem - }, - 'timeline/item/context': { - description: localize('view.timelineContext', "The Timeline view item context menu"), - type: 'array', - items: menuItem - }, + properties: index(apiMenus, menu => menu.key, menu => ({ + description: menu.proposed ? `(${localize('proposed', "Proposed API")}) ${menu.description}` : menu.description, + type: 'array', + items: menu.supportsSubmenus === false ? menuItem : { oneOf: [menuItem, submenuItem] } + })), + additionalProperties: { + description: 'Submenu', + type: 'array', + items: { oneOf: [menuItem, submenuItem] } } }; + export const submenusContribution: IJSONSchema = { + description: localize('vscode.extension.contributes.submenus', "(Proposed API) Contributes submenu items to the editor"), + type: 'array', + items: submenu + }; + // --- commands contribution point export interface IUserFriendlyCommand { @@ -430,74 +546,175 @@ commandsExtensionPoint.setHandler(extensions => { _commandRegistrations.add(MenuRegistry.addCommands(newCommands)); }); -const _menuRegistrations = new DisposableStore(); +interface IRegisteredSubmenu { + readonly id: MenuId; + readonly label: string; + readonly icon?: { dark: URI; light?: URI; } | ThemeIcon; +} -ExtensionsRegistry.registerExtensionPoint<{ [loc: string]: schema.IUserFriendlyMenuItem[] }>({ - extensionPoint: 'menus', - jsonSchema: schema.menusContribution -}).setHandler(extensions => { +const _submenus = new Map(); - // remove all previous menu registrations - _menuRegistrations.clear(); +const submenusExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'submenus', + jsonSchema: schema.submenusContribution +}); - const items: { id: MenuId, item: IMenuItem }[] = []; +submenusExtensionPoint.setHandler(extensions => { + + _submenus.clear(); for (let extension of extensions) { const { value, collector } = extension; forEach(value, entry => { - if (!schema.isValidMenuItems(entry.value, collector)) { + if (!schema.isValidSubmenu(entry.value, collector)) { return; } - const menu = schema.parseMenuId(entry.key); - if (typeof menu === 'undefined') { + if (!entry.value.id) { + collector.warn(localize('submenuId.invalid.id', "`{0}` is not a valid submenu identifier", entry.value.id)); + return; + } + if (!entry.value.label) { + collector.warn(localize('submenuId.invalid.label', "`{0}` is not a valid submenu label", entry.value.label)); + return; + } + + if (!extension.description.enableProposedApi) { + collector.error(localize('submenu.proposedAPI.invalid', "Submenus are proposed API and are only available when running out of dev or with the following command line switch: --enable-proposed-api {0}", extension.description.identifier.value)); + return; + } + + let absoluteIcon: { dark: URI; light?: URI; } | ThemeIcon | undefined; + if (entry.value.icon) { + if (typeof entry.value.icon === 'string') { + absoluteIcon = ThemeIcon.fromString(entry.value.icon) || { dark: resources.joinPath(extension.description.extensionLocation, entry.value.icon) }; + } else { + absoluteIcon = { + dark: resources.joinPath(extension.description.extensionLocation, entry.value.icon.dark), + light: resources.joinPath(extension.description.extensionLocation, entry.value.icon.light) + }; + } + } + + const item: IRegisteredSubmenu = { + id: new MenuId(`api:${entry.value.id}`), + label: entry.value.label, + icon: absoluteIcon + }; + + _submenus.set(entry.value.id, item); + }); + } +}); + +const _apiMenusByKey = new Map(Iterable.map(Iterable.from(apiMenus), menu => ([menu.key, menu]))); +const _menuRegistrations = new DisposableStore(); + +const menusExtensionPoint = ExtensionsRegistry.registerExtensionPoint<{ [loc: string]: (schema.IUserFriendlyMenuItem | schema.IUserFriendlySubmenuItem)[] }>({ + extensionPoint: 'menus', + jsonSchema: schema.menusContribution, + deps: [submenusExtensionPoint] +}); + +menusExtensionPoint.setHandler(extensions => { + + // remove all previous menu registrations + _menuRegistrations.clear(); + + const items: { id: MenuId, item: IMenuItem | ISubmenuItem }[] = []; + + for (let extension of extensions) { + const { value, collector } = extension; + + forEach(value, entry => { + if (!schema.isValidItems(entry.value, collector)) { + return; + } + + let menu = _apiMenusByKey.get(entry.key); + let isSubmenu = false; + + if (!menu) { + const submenu = _submenus.get(entry.key); + + if (submenu) { + menu = { + key: entry.key, + id: submenu.id, + description: '' + }; + isSubmenu = true; + } + } + + if (!menu) { collector.warn(localize('menuId.invalid', "`{0}` is not a valid menu identifier", entry.key)); return; } - if (schema.isProposedAPI(menu) && !extension.description.enableProposedApi) { + if (menu.proposed && !extension.description.enableProposedApi) { collector.error(localize('proposedAPI.invalid', "{0} is a proposed menu identifier and is only available when running out of dev or with the following command line switch: --enable-proposed-api {1}", entry.key, extension.description.identifier.value)); return; } - for (let item of entry.value) { - let command = MenuRegistry.getCommand(item.command); - let alt = item.alt && MenuRegistry.getCommand(item.alt) || undefined; + if (isSubmenu && !extension.description.enableProposedApi) { + collector.error(localize('proposedAPI.invalid.submenu', "{0} is a submenu identifier and is only available when running out of dev or with the following command line switch: --enable-proposed-api {1}", entry.key, extension.description.identifier.value)); + return; + } - if (!command) { - collector.error(localize('missing.command', "Menu item references a command `{0}` which is not defined in the 'commands' section.", item.command)); - continue; - } - if (item.alt && !alt) { - collector.warn(localize('missing.altCommand', "Menu item references an alt-command `{0}` which is not defined in the 'commands' section.", item.alt)); - } - if (item.command === item.alt) { - collector.info(localize('dupe.command', "Menu item references the same command as default and alt-command")); + for (const menuItem of entry.value) { + let item: IMenuItem | ISubmenuItem; + + if (schema.isMenuItem(menuItem)) { + const command = MenuRegistry.getCommand(menuItem.command); + const alt = menuItem.alt && MenuRegistry.getCommand(menuItem.alt) || undefined; + + if (!command) { + collector.error(localize('missing.command', "Menu item references a command `{0}` which is not defined in the 'commands' section.", menuItem.command)); + continue; + } + if (menuItem.alt && !alt) { + collector.warn(localize('missing.altCommand', "Menu item references an alt-command `{0}` which is not defined in the 'commands' section.", menuItem.alt)); + } + if (menuItem.command === menuItem.alt) { + collector.info(localize('dupe.command', "Menu item references the same command as default and alt-command")); + } + + item = { command, alt, group: undefined, order: undefined, when: undefined }; + } else { + if (!extension.description.enableProposedApi) { + collector.error(localize('proposedAPI.invalid.submenureference', "Menu item references a submenu which is only available when running out of dev or with the following command line switch: --enable-proposed-api {0}", extension.description.identifier.value)); + continue; + } + + if (menu.supportsSubmenus === false) { + collector.error(localize('proposedAPI.unsupported.submenureference', "Menu item references a submenu for a menu which doesn't have submenu support.")); + continue; + } + + const submenu = _submenus.get(menuItem.submenu); + + if (!submenu) { + collector.error(localize('missing.submenu', "Menu item references a submenu `{0}` which is not defined in the 'submenus' section.", menuItem.submenu)); + continue; + } + + item = { submenu: submenu.id, icon: submenu.icon, title: submenu.label, group: undefined, order: undefined, when: undefined }; } - let group: string | undefined; - let order: number | undefined; - if (item.group) { - const idx = item.group.lastIndexOf('@'); + if (menuItem.group) { + const idx = menuItem.group.lastIndexOf('@'); if (idx > 0) { - group = item.group.substr(0, idx); - order = Number(item.group.substr(idx + 1)) || undefined; + item.group = menuItem.group.substr(0, idx); + item.order = Number(menuItem.group.substr(idx + 1)) || undefined; } else { - group = item.group; + item.group = menuItem.group; } } - items.push({ - id: menu, - item: { - command, - alt, - group, - order, - when: ContextKeyExpr.deserialize(item.when) - } - }); + item.when = ContextKeyExpr.deserialize(menuItem.when); + items.push({ id: menu.id, item }); } }); } diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 3a02c5ce0b7..8558835c744 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -13,6 +13,7 @@ import { ExtHostDownloadService } from 'vs/workbench/api/node/extHostDownloadSer 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'; class NodeModuleRequireInterceptor extends RequireInterceptor { @@ -76,6 +77,10 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { }; } + protected _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined { + return extensionDescription.main; + } + protected _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { if (module.scheme !== Schemas.file) { throw new Error(`Cannot load URI: '${module}', must be of file-scheme`); diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index ad10c7cb61e..0c59bbd7a6f 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -47,6 +47,7 @@ export class ExtHostTask extends ExtHostTaskBase { platform: process.platform }); } + this._proxy.$registerSupportedExecutions(true, true, true); } public async executeTask(extension: IExtensionDescription, task: vscode.Task): Promise { diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 462038c5d36..c777688550a 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -200,7 +200,7 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { this._proxy.$sendResolvedLaunchConfig(id, shellLaunchConfig); // Fork the process and listen for messages - this._logService.debug(`Terminal process launching on ext host`, shellLaunchConfig, initialCwd, cols, rows, env); + this._logService.debug(`Terminal process launching on ext host`, { shellLaunchConfig, initialCwd, cols, rows, env }); // TODO: Support conpty on remote, it doesn't seem to work for some reason? // TODO: When conpty is enabled, only enable it when accessibilityMode is off const enableConpty = false; //terminalConfig.get('windowsEnableConpty') as boolean; diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts index dd8f4e1fe7c..c71ab1c7da4 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -8,6 +8,7 @@ import { ExtensionActivationTimesBuilder } from 'vs/workbench/api/common/extHost import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { URI } from 'vs/base/common/uri'; import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; class WorkerRequireInterceptor extends RequireInterceptor { @@ -40,6 +41,10 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { await this._fakeModules.install(); } + protected _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined { + return extensionDescription.browser; + } + protected async _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { module = module.with({ path: ensureSuffix(module.path, '.js') }); @@ -51,7 +56,10 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { // fetch JS sources as text and create a new function around it const source = await response.text(); - const initFn = new Function('module', 'exports', 'require', `${source}\n//# sourceURL=${module.toString(true)}`); + // Here we append #vscode-extension to serve as a marker, such that source maps + // can be adjusted for the extra wrapping function. + const sourceURL = `${module.toString(true)}#vscode-extension`; + const initFn = new Function('module', 'exports', 'require', `${source}\n//# sourceURL=${sourceURL}`); // define commonjs globals: `module`, `exports`, and `require` const _exports = {}; diff --git a/src/vs/workbench/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts index 555d2c2f097..c190bf33cc7 100644 --- a/src/vs/workbench/browser/actions/listCommands.ts +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -523,7 +523,6 @@ function focusElement(accessor: ServicesAccessor, retainCurrentFocus: boolean): } } tree.setSelection(focus, fakeKeyboardEvent); - tree.open(fakeKeyboardEvent); } } @@ -655,18 +654,17 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const focused = accessor.get(IListService).lastFocusedList; // Tree only - if (focused && !(focused instanceof List || focused instanceof PagedList)) { - if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { - const tree = focused; - const focus = tree.getFocus(); - - if (focus.length === 0) { - return; - } + if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { + const tree = focused; + const focus = tree.getFocus(); + if (focus.length > 0 && tree.isCollapsible(focus[0])) { tree.toggleCollapsed(focus[0]); + return; } } + + focusElement(accessor, true); } }); diff --git a/src/vs/workbench/browser/actions/navigationActions.ts b/src/vs/workbench/browser/actions/navigationActions.ts index 619ceaf8ede..9ea421e1256 100644 --- a/src/vs/workbench/browser/actions/navigationActions.ts +++ b/src/vs/workbench/browser/actions/navigationActions.ts @@ -12,10 +12,13 @@ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/bro import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { IPanel } from 'vs/workbench/common/panel'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId, registerAction2, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { Direction } from 'vs/base/browser/ui/grid/grid'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; abstract class BaseNavigationAction extends Action { @@ -257,12 +260,38 @@ export class FocusPreviousPart extends Action { } } -const registry = Registry.as(Extensions.WorkbenchActions); +class GoHomeContributor implements IWorkbenchContribution { + + constructor( + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService + ) { + const homeIndicator = environmentService.options?.homeIndicator; + if (homeIndicator) { + registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.goHome`, + title: nls.localize('goHome', "Go Home"), + menu: { id: MenuId.MenubarWebNavigationMenu } + }); + } + async run(): Promise { + window.location.href = homeIndicator.href; + } + }); + } + } +} + +const actionsRegistry = Registry.as(Extensions.WorkbenchActions); const viewCategory = nls.localize('view', "View"); -registry.registerWorkbenchAction(SyncActionDescriptor.from(NavigateUpAction, undefined), 'View: Navigate to the View Above', viewCategory); -registry.registerWorkbenchAction(SyncActionDescriptor.from(NavigateDownAction, undefined), 'View: Navigate to the View Below', viewCategory); -registry.registerWorkbenchAction(SyncActionDescriptor.from(NavigateLeftAction, undefined), 'View: Navigate to the View on the Left', viewCategory); -registry.registerWorkbenchAction(SyncActionDescriptor.from(NavigateRightAction, undefined), 'View: Navigate to the View on the Right', viewCategory); -registry.registerWorkbenchAction(SyncActionDescriptor.from(FocusNextPart, { primary: KeyCode.F6 }), 'View: Focus Next Part', viewCategory); -registry.registerWorkbenchAction(SyncActionDescriptor.from(FocusPreviousPart, { primary: KeyMod.Shift | KeyCode.F6 }), 'View: Focus Previous Part', viewCategory); +actionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(NavigateUpAction, undefined), 'View: Navigate to the View Above', viewCategory); +actionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(NavigateDownAction, undefined), 'View: Navigate to the View Below', viewCategory); +actionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(NavigateLeftAction, undefined), 'View: Navigate to the View on the Left', viewCategory); +actionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(NavigateRightAction, undefined), 'View: Navigate to the View on the Right', viewCategory); +actionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(FocusNextPart, { primary: KeyCode.F6 }), 'View: Focus Next Part', viewCategory); +actionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(FocusPreviousPart, { primary: KeyMod.Shift | KeyCode.F6 }), 'View: Focus Previous Part', viewCategory); + +const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchRegistry.registerWorkbenchContribution(GoHomeContributor, LifecyclePhase.Ready); diff --git a/src/vs/workbench/browser/actions/textInputActions.ts b/src/vs/workbench/browser/actions/textInputActions.ts index b9ae0528184..da382fb4df2 100644 --- a/src/vs/workbench/browser/actions/textInputActions.ts +++ b/src/vs/workbench/browser/actions/textInputActions.ts @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IAction, Action } from 'vs/base/common/actions'; +import { IAction, Action, Separator } from 'vs/base/common/actions'; import { localize } from 'vs/nls'; -import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Disposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/browser/composite.ts b/src/vs/workbench/browser/composite.ts index afda9848e3c..16f7cf4632e 100644 --- a/src/vs/workbench/browser/composite.ts +++ b/src/vs/workbench/browser/composite.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IAction, IActionRunner, ActionRunner } from 'vs/base/common/actions'; -import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IAction, IActionRunner, ActionRunner, IActionViewItem } from 'vs/base/common/actions'; import { Component } from 'vs/workbench/common/component'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IComposite, ICompositeControl } from 'vs/workbench/common/composite'; diff --git a/src/vs/workbench/browser/panecomposite.ts b/src/vs/workbench/browser/panecomposite.ts index 279916417ec..5279b52e21b 100644 --- a/src/vs/workbench/browser/panecomposite.ts +++ b/src/vs/workbench/browser/panecomposite.ts @@ -15,10 +15,9 @@ import { Composite } from 'vs/workbench/browser/composite'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ViewPaneContainer } from './parts/views/viewPaneContainer'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; -import { IAction, IActionViewItem } from 'vs/base/common/actions'; +import { IAction, IActionViewItem, Separator } from 'vs/base/common/actions'; import { ViewContainerMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; import { MenuId } from 'vs/platform/actions/common/actions'; -import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; export class PaneComposite extends Composite implements IPaneComposite { @@ -66,6 +65,10 @@ export class PaneComposite extends Composite implements IPaneComposite { return this.viewPaneContainer; } + getActionsContext(): unknown { + return this.getViewPaneContainer().getActionsContext(); + } + getContextMenuActions(): ReadonlyArray { const result = []; result.push(...this.menuActions.getContextMenuActions()); diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index a623aee383a..ff18883e95e 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; -import { Action, IAction } from 'vs/base/common/actions'; +import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; import { KeyCode } from 'vs/base/common/keyCodes'; import { dispose } from 'vs/base/common/lifecycle'; import { SyncActionDescriptor, IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; @@ -27,12 +27,13 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { Codicon } from 'vs/base/common/codicons'; -import { ActionViewItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { isMacintosh } from 'vs/base/common/platform'; -import { ContextSubMenu } from 'vs/base/browser/contextmenu'; import { 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'; export class ViewContainerActivityAction extends ActivityAction { @@ -41,6 +42,7 @@ export class ViewContainerActivityAction extends ActivityAction { private readonly viewletService: IViewletService; private readonly layoutService: IWorkbenchLayoutService; private readonly telemetryService: ITelemetryService; + private readonly configurationService: IConfigurationService; private lastRun: number; @@ -48,7 +50,8 @@ export class ViewContainerActivityAction extends ActivityAction { activity: IActivity, @IViewletService viewletService: IViewletService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, - @ITelemetryService telemetryService: ITelemetryService + @ITelemetryService telemetryService: ITelemetryService, + @IConfigurationService configurationService: IConfigurationService ) { super(activity); @@ -56,6 +59,7 @@ export class ViewContainerActivityAction extends ActivityAction { this.viewletService = viewletService; this.layoutService = layoutService; this.telemetryService = telemetryService; + this.configurationService = configurationService; } updateActivity(activity: IActivity): void { @@ -76,11 +80,22 @@ export class ViewContainerActivityAction extends ActivityAction { const sideBarVisible = this.layoutService.isVisible(Parts.SIDEBAR_PART); const activeViewlet = this.viewletService.getActiveViewlet(); + const focusBehavior = this.configurationService.getValue('workbench.activityBar.iconClickBehavior'); - // Hide sidebar if selected viewlet already visible if (sideBarVisible && activeViewlet?.getId() === this.activity.id) { - this.logAction('hide'); - this.layoutService.setSideBarHidden(true); + switch (focusBehavior) { + case 'focus': + this.logAction('refocus'); + this.viewletService.openViewlet(this.activity.id, true); + break; + case 'toggle': + default: + // Hide sidebar if selected viewlet already visible + this.logAction('hide'); + this.layoutService.setSideBarHidden(true); + break; + } + return; } @@ -98,6 +113,8 @@ export class ViewContainerActivityAction extends ActivityAction { } } +export const ACCOUNTS_VISIBILITY_PREFERENCE_KEY = 'workbench.activity.showAccounts'; + export class AccountsActionViewItem extends ActivityActionViewItem { constructor( action: ActivityAction, @@ -107,7 +124,8 @@ export class AccountsActionViewItem extends ActivityActionViewItem { @IMenuService protected menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IStorageService private readonly storageService: IStorageService ) { super(action, { draggable: false, colors, icon: true }, themeService); } @@ -159,7 +177,7 @@ export class AccountsActionViewItem extends ActivityActionViewItem { }); const result = await Promise.all(allSessions); - let menus: (IAction | ContextSubMenu)[] = []; + let menus: IAction[] = []; result.forEach(sessionInfo => { const providerDisplayName = this.authenticationService.getLabel(sessionInfo.providerId); Object.keys(sessionInfo.sessions).forEach(accountName => { @@ -173,7 +191,7 @@ export class AccountsActionViewItem extends ActivityActionViewItem { const actions = hasEmbedderAccountSession ? [manageExtensionsAction] : [manageExtensionsAction, signOutAction]; - const menu = new ContextSubMenu(`${accountName} (${providerDisplayName})`, actions); + const menu = new SubmenuAction('activitybar.submenu', `${accountName} (${providerDisplayName})`, actions); menus.push(menu); }); }); @@ -190,6 +208,15 @@ export class AccountsActionViewItem extends ActivityActionViewItem { } }); + if (menus.length) { + menus.push(new Separator()); + } + + menus.push(new Action('hide', nls.localize('hide', "Hide"), undefined, true, _ => { + this.storageService.store(ACCOUNTS_VISIBILITY_PREFERENCE_KEY, false, StorageScope.GLOBAL); + return Promise.resolve(); + })); + return menus; } diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 87e4aabcf4b..1d8b7d7999d 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { GLOBAL_ACTIVITY_ID, IActivity, ACCOUNTS_ACTIIVTY_ID } from 'vs/workbench/common/activity'; import { Part } from 'vs/workbench/browser/part'; -import { GlobalActivityActionViewItem, ViewContainerActivityAction, PlaceHolderToggleCompositePinnedAction, PlaceHolderViewContainerActivityAction, AccountsActionViewItem, HomeAction, HomeActionViewItem } from 'vs/workbench/browser/parts/activitybar/activitybarActions'; +import { GlobalActivityActionViewItem, ViewContainerActivityAction, PlaceHolderToggleCompositePinnedAction, PlaceHolderViewContainerActivityAction, AccountsActionViewItem, HomeAction, HomeActionViewItem, ACCOUNTS_VISIBILITY_PREFERENCE_KEY } from 'vs/workbench/browser/parts/activitybar/activitybarActions'; import { IBadge, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -37,7 +37,7 @@ import { isWeb } from 'vs/base/common/platform'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { Before2D } from 'vs/workbench/browser/dnd'; import { Codicon, iconRegistry } from 'vs/base/common/codicons'; -import { Action } from 'vs/base/common/actions'; +import { Action, Separator } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -75,7 +75,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { static readonly PINNED_VIEW_CONTAINERS = 'workbench.activity.pinnedViewlets2'; private static readonly PLACEHOLDER_VIEW_CONTAINERS = 'workbench.activity.placeholderViewlets'; private static readonly HOME_BAR_VISIBILITY_PREFERENCE = 'workbench.activity.showHomeIndicator'; - + private static readonly ACCOUNTS_ACTION_INDEX = 0; //#region IView readonly minimumWidth: number = 48; @@ -103,6 +103,8 @@ export class ActivitybarPart extends Part implements IActivityBarService { private accountsActivityAction: ActivityAction | undefined; + private accountsActivity: ICompositeActivity[] = []; + private readonly compositeActions = new Map(); private readonly viewContainerDisposables = new Map(); @@ -164,6 +166,18 @@ export class ActivitybarPart extends Part implements IActivityBarService { actions.push(this.instantiationService.createInstance(ToggleMenuBarAction, ToggleMenuBarAction.ID, menuBarVisibility === 'compact' ? nls.localize('hideMenu', "Hide Menu") : nls.localize('showMenu', "Show Menu"))); } + const toggleAccountsVisibilityAction = new Action( + 'toggleAccountsVisibility', + nls.localize('accounts', "Accounts"), + undefined, + true, + async () => { this.accountsVisibilityPreference = !this.accountsVisibilityPreference; } + ); + + toggleAccountsVisibilityAction.checked = !!this.accountsActivityAction; + actions.push(toggleAccountsVisibilityAction); + actions.push(new Separator()); + actions.push(new Action( ToggleActivityBarVisibilityAction.ID, nls.localize('hideActivitBar', "Hide Activity Bar"), @@ -307,65 +321,68 @@ export class ActivitybarPart extends Part implements IActivityBarService { } if (viewContainerOrActionId === GLOBAL_ACTIVITY_ID) { - return this.showGlobalActivity(badge, clazz, priority); + return this.showGlobalActivity(GLOBAL_ACTIVITY_ID, badge, clazz, priority); } if (viewContainerOrActionId === ACCOUNTS_ACTIIVTY_ID) { - if (this.accountsActivityAction) { - this.accountsActivityAction.setBadge(badge, clazz); - - return toDisposable(() => this.accountsActivityAction?.setBadge(undefined)); - } + return this.showGlobalActivity(ACCOUNTS_ACTIIVTY_ID, badge, clazz, priority); } return Disposable.None; } - private showGlobalActivity(badge: IBadge, clazz?: string, priority?: number): IDisposable { + private showGlobalActivity(activityId: string, badge: IBadge, clazz?: string, priority?: number): IDisposable { if (typeof priority !== 'number') { priority = 0; } const activity: ICompositeActivity = { badge, clazz, priority }; + const activityCache = activityId === GLOBAL_ACTIVITY_ID ? this.globalActivity : this.accountsActivity; - for (let i = 0; i <= this.globalActivity.length; i++) { - if (i === this.globalActivity.length) { - this.globalActivity.push(activity); + for (let i = 0; i <= activityCache.length; i++) { + if (i === activityCache.length) { + activityCache.push(activity); break; - } else if (this.globalActivity[i].priority <= priority) { - this.globalActivity.splice(i, 0, activity); + } else if (activityCache[i].priority <= priority) { + activityCache.splice(i, 0, activity); break; } } - this.updateGlobalActivity(); + this.updateGlobalActivity(activityId); - return toDisposable(() => this.removeGlobalActivity(activity)); + return toDisposable(() => this.removeGlobalActivity(activityId, activity)); } - private removeGlobalActivity(activity: ICompositeActivity): void { - const index = this.globalActivity.indexOf(activity); + private removeGlobalActivity(activityId: string, activity: ICompositeActivity): void { + const activityCache = activityId === GLOBAL_ACTIVITY_ID ? this.globalActivity : this.accountsActivity; + const index = activityCache.indexOf(activity); if (index !== -1) { - this.globalActivity.splice(index, 1); - this.updateGlobalActivity(); + activityCache.splice(index, 1); + this.updateGlobalActivity(activityId); } } - private updateGlobalActivity(): void { - const globalActivityAction = assertIsDefined(this.globalActivityAction); - if (this.globalActivity.length) { - const [{ badge, clazz, priority }] = this.globalActivity; - if (badge instanceof NumberBadge && this.globalActivity.length > 1) { - const cumulativeNumberBadge = this.getCumulativeNumberBadge(priority); - globalActivityAction.setBadge(cumulativeNumberBadge); + private updateGlobalActivity(activityId: string): void { + const activityAction = activityId === GLOBAL_ACTIVITY_ID ? this.globalActivityAction : this.accountsActivityAction; + if (!activityAction) { + return; + } + + const activityCache = activityId === GLOBAL_ACTIVITY_ID ? this.globalActivity : this.accountsActivity; + if (activityCache.length) { + const [{ badge, clazz, priority }] = activityCache; + if (badge instanceof NumberBadge && activityCache.length > 1) { + const cumulativeNumberBadge = this.getCumulativeNumberBadge(activityCache, priority); + activityAction.setBadge(cumulativeNumberBadge); } else { - globalActivityAction.setBadge(badge, clazz); + activityAction.setBadge(badge, clazz); } } else { - globalActivityAction.setBadge(undefined); + activityAction.setBadge(undefined); } } - private getCumulativeNumberBadge(priority: number): NumberBadge { - const numberActivities = this.globalActivity.filter(activity => activity.badge instanceof NumberBadge && activity.priority === priority); + private getCumulativeNumberBadge(activityCache: ICompositeActivity[], priority: number): NumberBadge { + const numberActivities = activityCache.filter(activity => activity.badge instanceof NumberBadge && activity.priority === priority); let number = numberActivities.reduce((result, activity) => { return result + (activity.badge).number; }, 0); let descriptorFn = (): string => { return numberActivities.reduce((result, activity, index) => { @@ -587,17 +604,37 @@ export class ActivitybarPart extends Part implements IActivityBarService { cssClass: Codicon.settingsGear.classNames }); - this.accountsActivityAction = new ActivityAction({ - id: 'workbench.actions.accounts', - name: nls.localize('accounts', "Accounts"), - cssClass: Codicon.account.classNames - }); + if (this.accountsVisibilityPreference) { + this.accountsActivityAction = new ActivityAction({ + id: 'workbench.actions.accounts', + name: nls.localize('accounts', "Accounts"), + cssClass: Codicon.account.classNames + }); - this.globalActivityActionBar.push(this.accountsActivityAction); + this.globalActivityActionBar.push(this.accountsActivityAction, { index: ActivitybarPart.ACCOUNTS_ACTION_INDEX }); + } this.globalActivityActionBar.push(this.globalActivityAction); } + private toggleAccountsActivity() { + if (this.globalActivityActionBar) { + if (this.accountsActivityAction) { + this.globalActivityActionBar.pull(ActivitybarPart.ACCOUNTS_ACTION_INDEX); + this.accountsActivityAction = undefined; + } else { + this.accountsActivityAction = new ActivityAction({ + id: 'workbench.actions.accounts', + name: nls.localize('accounts', "Accounts"), + cssClass: Codicon.account.classNames + }); + this.globalActivityActionBar.push(this.accountsActivityAction, { index: ActivitybarPart.ACCOUNTS_ACTION_INDEX }); + } + } + + this.updateGlobalActivity(ACCOUNTS_ACTIIVTY_ID); + } + private getCompositeActions(compositeId: string): { activityAction: ViewContainerActivityAction, pinnedAction: ToggleCompositePinnedAction } { let compositeActions = this.compositeActions.get(compositeId); if (!compositeActions) { @@ -827,6 +864,10 @@ export class ActivitybarPart extends Part implements IActivityBarService { if (e.key === ActivitybarPart.HOME_BAR_VISIBILITY_PREFERENCE && e.scope === StorageScope.GLOBAL) { this.onDidChangeHomeBarVisibility(); } + + if (e.key === ACCOUNTS_VISIBILITY_PREFERENCE_KEY && e.scope === StorageScope.GLOBAL) { + this.toggleAccountsActivity(); + } } private saveCachedViewContainers(): void { @@ -964,6 +1005,14 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.storageService.store(ActivitybarPart.HOME_BAR_VISIBILITY_PREFERENCE, value, StorageScope.GLOBAL); } + private get accountsVisibilityPreference(): boolean { + return this.storageService.getBoolean(ACCOUNTS_VISIBILITY_PREFERENCE_KEY, StorageScope.GLOBAL, true); + } + + private set accountsVisibilityPreference(value: boolean) { + this.storageService.store(ACCOUNTS_VISIBILITY_PREFERENCE_KEY, value, StorageScope.GLOBAL); + } + private migrateFromOldCachedViewContainersValue(): void { const value = this.storageService.get('workbench.activity.pinnedViewlets', StorageScope.GLOBAL); if (value !== undefined) { diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index f2b05614016..f029a2c190c 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { Action, IAction } from 'vs/base/common/actions'; +import { Action, IAction, Separator } from 'vs/base/common/actions'; import { illegalArgument } from 'vs/base/common/errors'; import * as arrays from 'vs/base/common/arrays'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IBadge } from 'vs/workbench/services/activity/common/activity'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ActionBar, ActionsOrientation, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { CompositeActionViewItem, CompositeOverflowActivityAction, ICompositeActivity, CompositeOverflowActivityActionViewItem, ActivityAction, ICompositeBar, ICompositeBarColors } from 'vs/workbench/browser/parts/compositeBarActions'; import { Dimension, $, addDisposableListener, EventType, EventHelper, toggleClass, isAncestor } from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -629,7 +629,7 @@ export class CompositeBar extends Widget implements ICompositeBar { }); } - private getContextMenuActions(): ReadonlyArray { + private getContextMenuActions(): IAction[] { const actions: IAction[] = this.model.visibleItems .map(({ id, name, activityAction }) => ({ id, diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index e4d7358167e..9f358b6e25d 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { Action } from 'vs/base/common/actions'; +import { Action, Separator } from 'vs/base/common/actions'; import * as dom from 'vs/base/browser/dom'; -import { BaseActionViewItem, IBaseActionViewItemOptions, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { dispose, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -21,6 +20,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { CompositeDragAndDropObserver, ICompositeDragAndDrop, Before2D, toggleDropEffect } from 'vs/workbench/browser/dnd'; import { Color } from 'vs/base/common/color'; import { Codicon } from 'vs/base/common/codicons'; +import { IBaseActionViewItemOptions, BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; export interface ICompositeActivity { badge: IBadge; diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 09a3e5b4ccf..57adb998ecc 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -11,9 +11,9 @@ import * as strings from 'vs/base/common/strings'; import { Emitter } from 'vs/base/common/event'; import * as errors from 'vs/base/common/errors'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IActionViewItem, ActionsOrientation, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionsOrientation, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; -import { IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; +import { IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, IActionViewItem } from 'vs/base/common/actions'; import { Part, IPartOptions } from 'vs/workbench/browser/part'; import { Composite, CompositeRegistry } from 'vs/workbench/browser/composite'; import { IComposite } from 'vs/workbench/common/composite'; diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index b961c0fe0fe..14dca63bc68 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -32,7 +32,8 @@ import { Selection } from 'vs/editor/common/core/selection'; import { TabFocus } from 'vs/editor/common/config/commonEditorConfig'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { ITextFileService, SUPPORTED_ENCODINGS } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { SUPPORTED_ENCODINGS } from 'vs/workbench/services/textfile/common/encoding'; import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { ConfigurationChangedEvent, IEditorOptions, EditorOption } from 'vs/editor/common/config/editorOptions'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index c4865014080..c693429a87d 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -7,16 +7,16 @@ import 'vs/css!./media/titlecontrol'; import { applyDragImage, DataTransfers } from 'vs/base/browser/dnd'; import { addDisposableListener, Dimension, EventType } from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { ActionsOrientation, IActionViewItem, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionsOrientation, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IAction, IRunEvent, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; +import { IAction, IRunEvent, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, IActionViewItem } from 'vs/base/common/actions'; import * as arrays from 'vs/base/common/arrays'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; import { dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { getCodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { localize } from 'vs/nls'; -import { createActionViewItem, createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { ExecuteCommandAction, IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { createAndFillInActionBarActions, createAndFillInContextMenuActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { ExecuteCommandAction, IMenu, IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -163,17 +163,22 @@ export abstract class TitleControl extends Themable { const activeEditorPane = this.group.activeEditorPane; // Check Active Editor - let actionViewItem: IActionViewItem | undefined = undefined; if (activeEditorPane instanceof BaseEditor) { - actionViewItem = activeEditorPane.getActionViewItem(action); + const result = activeEditorPane.getActionViewItem(action); + + if (result) { + return result; + } } // Check extensions - if (!actionViewItem) { - actionViewItem = createActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + if (action instanceof MenuItemAction) { + return this.instantiationService.createInstance(MenuEntryActionViewItem, action); + } else if (action instanceof SubmenuItemAction) { + return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); } - return actionViewItem; + return undefined; } protected updateEditorActionsToolbar(): void { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 4b213eb468a..567ec2a4a1b 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -16,7 +16,6 @@ import { IAction, IActionRunner } from 'vs/base/common/actions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { dispose, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdown'; import { INotificationViewItem, NotificationViewItem, NotificationViewItemContentChangeKind, INotificationMessage, ChoiceAction } from 'vs/workbench/common/notifications'; import { ClearNotificationAction, ExpandNotificationAction, CollapseNotificationAction, ConfigureNotificationAction } from 'vs/workbench/browser/parts/notifications/notificationsActions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -24,6 +23,7 @@ import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { Severity } from 'vs/platform/notification/common/notification'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { Codicon } from 'vs/base/common/codicons'; +import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; export class NotificationsListDelegate implements IListVirtualDelegate { @@ -211,7 +211,7 @@ export class NotificationRenderer implements IListRenderer { if (action && action instanceof ConfigureNotificationAction) { - const item = new DropdownMenuActionViewItem(action, action.configurationActions, this.contextMenuService, undefined, this.actionRunner, undefined, action.class); + const item = new DropdownMenuActionViewItem(action, action.configurationActions, this.contextMenuService, { actionRunner: this.actionRunner, classNames: action.class }); data.toDispose.add(item); return item; diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index fd6e67fbc09..e29be7ab5a0 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -14,7 +14,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { StatusbarAlignment, IStatusbarService, IStatusbarEntry, IStatusbarEntryAccessor } from 'vs/workbench/services/statusbar/common/statusbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { Action, IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; +import { Action, IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, Separator } from 'vs/base/common/actions'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector, ThemeColor, HIGH_CONTRAST } from 'vs/platform/theme/common/themeService'; import { STATUS_BAR_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_ITEM_ACTIVE_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND, STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND, STATUS_BAR_BORDER, STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_NO_FOLDER_BORDER } from 'vs/workbench/common/theme'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; @@ -29,7 +29,6 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { coalesce } from 'vs/base/common/arrays'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ToggleStatusbarVisibilityAction } from 'vs/workbench/browser/actions/layoutActions'; -import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { assertIsDefined } from 'vs/base/common/types'; import { Emitter } from 'vs/base/common/event'; import { Command } from 'vs/editor/common/modes'; diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index 981dda9ed98..21f96d64552 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -8,8 +8,7 @@ import { IMenuService, MenuId, IMenu, SubmenuItemAction, registerAction2, Action import { registerThemingParticipant, IColorTheme, ICssStyleCollector, IThemeService } from 'vs/platform/theme/common/themeService'; import { MenuBarVisibility, getTitleBarStyle, IWindowOpenable, getMenuBarVisibility } from 'vs/platform/windows/common/windows'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IAction, Action } from 'vs/base/common/actions'; -import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IAction, Action, SubmenuAction, Separator } from 'vs/base/common/actions'; import * as DOM from 'vs/base/browser/dom'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { isMacintosh, isWeb, isIOS } from 'vs/base/common/platform'; @@ -27,7 +26,7 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { MenuBar, IMenuBarOptions } from 'vs/base/browser/ui/menu/menubar'; -import { SubmenuAction, Direction } from 'vs/base/browser/ui/menu/menu'; +import { Direction } from 'vs/base/browser/ui/menu/menu'; import { attachMenuStyler } from 'vs/platform/theme/common/styler'; import { mnemonicMenuLabel, unmnemonicLabel } from 'vs/base/common/labels'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; @@ -599,7 +598,7 @@ export class CustomMenubarControl extends MenubarControl { const submenuActions: SubmenuAction[] = []; updateActions(submenu, submenuActions, topLevelTitle); - target.push(new SubmenuAction(mnemonicMenuLabel(action.label), submenuActions)); + target.push(new SubmenuAction(action.id, mnemonicMenuLabel(action.label), submenuActions)); } else { action.label = mnemonicMenuLabel(this.calculateActionLabel(action)); target.push(action); diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index ac3577c8241..028a1594494 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -12,8 +12,8 @@ import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, import { 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 } from 'vs/base/common/actions'; -import { IActionViewItem, ActionsOrientation, Separator, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IAction, Separator, IActionViewItem } from 'vs/base/common/actions'; +import { ActionsOrientation, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; import { Registry } from 'vs/platform/registry/common/platform'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -33,8 +33,8 @@ import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewl import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { Component } from 'vs/workbench/common/component'; -import { MenuId, MenuItemAction, registerAction2, Action2, IAction2Options } from 'vs/platform/actions/common/actions'; -import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { MenuId, MenuItemAction, registerAction2, Action2, IAction2Options, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { ViewMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; import { parseLinkedText } from 'vs/base/common/linkedText'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -482,7 +482,9 @@ export abstract class ViewPane extends Pane implements IView { getActionViewItem(action: IAction): IActionViewItem | undefined { if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(ContextAwareMenuEntryActionViewItem, action); + return this.instantiationService.createInstance(MenuEntryActionViewItem, action); + } else if (action instanceof SubmenuItemAction) { + return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); } return undefined; } @@ -1068,6 +1070,10 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { return []; } + getActionsContext(): unknown { + return undefined; + } + getViewsVisibilityActions(): IAction[] { return this.viewContainerModel.activeViewDescriptors.map(viewDescriptor => ({ id: `${viewDescriptor.id}.toggleVisibility`, diff --git a/src/vs/workbench/browser/viewlet.ts b/src/vs/workbench/browser/viewlet.ts index c222e990bcb..5cdb3864c88 100644 --- a/src/vs/workbench/browser/viewlet.ts +++ b/src/vs/workbench/browser/viewlet.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; -import { Action, IAction } from 'vs/base/common/actions'; +import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { CompositeDescriptor, CompositeRegistry } from 'vs/workbench/browser/composite'; @@ -26,8 +26,6 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { PaneComposite } from 'vs/workbench/browser/panecomposite'; -import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { ContextSubMenu } from 'vs/base/browser/contextmenu'; import { Event } from 'vs/base/common/event'; export abstract class Viewlet extends PaneComposite implements IViewlet { @@ -68,18 +66,18 @@ export abstract class Viewlet extends PaneComposite implements IViewlet { } getSecondaryActions(): IAction[] { - const viewSecondaryActions = this.viewPaneContainer.getViewsVisibilityActions(); + const viewVisibilityActions = this.viewPaneContainer.getViewsVisibilityActions(); const secondaryActions = this.viewPaneContainer.getSecondaryActions(); - if (viewSecondaryActions.length <= 1) { + if (viewVisibilityActions.length <= 1 || viewVisibilityActions.every(({ enabled }) => !enabled)) { return secondaryActions; } if (secondaryActions.length === 0) { - return viewSecondaryActions; + return viewVisibilityActions; } return [ - new ContextSubMenu(nls.localize('views', "Views"), viewSecondaryActions), + new SubmenuAction('workbench.views', nls.localize('views', "Views"), viewVisibilityActions), new Separator(), ...secondaryActions ]; diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 18196e719a3..c482eae8ef3 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -22,7 +22,7 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio enum: ['default', 'large'], enumDescriptions: [ nls.localize('workbench.editor.titleScrollbarSizing.default', "The default size."), - nls.localize('workbench.editor.titleScrollbarSizing.large', "Increases the size, so it can be grabed more easily with the mouse") + nls.localize('workbench.editor.titleScrollbarSizing.large', "Increases the size, so it can be grabbed more easily with the mouse") ], description: nls.localize('tabScrollbarHeight', "Controls the height of the scrollbars used for tabs and breadcrumbs in the editor title area."), default: 'default', @@ -231,6 +231,16 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio 'default': true, 'description': nls.localize('activityBarVisibility', "Controls the visibility of the activity bar in the workbench.") }, + 'workbench.activityBar.iconClickBehavior': { + 'type': 'string', + 'enum': ['toggle', 'focus'], + 'default': 'toggle', + 'description': nls.localize('activityBarIconClickBehavior', "Controls the behavior of clicking an activity bar icon in the workbench."), + 'enumDescriptions': [ + nls.localize('workbench.activityBar.iconClickBehavior.toggle', "Hide the side bar if the clicked item is already visible."), + nls.localize('workbench.activityBar.iconClickBehavior.focus', "Focus side bar if the clicked item is already visible.") + ] + }, 'workbench.view.alwaysShowHeaderActions': { 'type': 'boolean', 'default': false, diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 8726ead6be5..b0712b4e90b 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -465,6 +465,8 @@ export interface IView { readonly id: string; + focus(): void; + isVisible(): boolean; isBodyVisible(): boolean; @@ -716,6 +718,7 @@ export interface IViewPaneContainer { getActions(): IAction[]; getSecondaryActions(): IAction[]; getActionViewItem(action: IAction): IActionViewItem | undefined; + getActionsContext(): unknown; getView(viewId: string): IView | undefined; saveState(): void; } diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts index fde0bb6bc1a..e51a91dc96b 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts @@ -32,9 +32,9 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { Color } from 'vs/base/common/color'; import { TreeMouseEventTarget, ITreeNode } from 'vs/base/browser/ui/tree/tree'; import { URI } from 'vs/base/common/uri'; -import { MenuId, IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { MenuId, IMenuService } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { createAndFillInActionBarActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; const enum State { Loading = 'loading', @@ -94,7 +94,7 @@ export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget { @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { - super(editor, { showFrame: true, showArrow: true, isResizeable: true, isAccessible: true }); + super(editor, { showFrame: true, showArrow: true, isResizeable: true, isAccessible: true }, _instantiationService); this.create(); this._peekViewService.addExclusiveWidget(editor, this); this._applyTheme(themeService.getColorTheme()); @@ -142,8 +142,8 @@ export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget { protected _getActionBarOptions(): IActionBarOptions { return { - orientation: ActionsOrientation.HORIZONTAL, - actionViewItemProvider: action => action instanceof MenuItemAction ? this._instantiationService.createInstance(MenuEntryActionViewItem, action) : undefined + ...super._getActionBarOptions(), + orientation: ActionsOrientation.HORIZONTAL }; } diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.ts index 1d171e60334..088c76bdb4b 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.ts @@ -43,7 +43,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { private readonly prevBtn: SimpleButton; private readonly nextBtn: SimpleButton; - private readonly _replaceInput!: ReplaceInput; + protected readonly _replaceInput!: ReplaceInput; private readonly _innerReplaceDomNode!: HTMLElement; private _toggleReplaceBtn!: SimpleButton; private readonly _replaceInputFocusTracker!: dom.IFocusTracker; @@ -372,6 +372,34 @@ export abstract class SimpleFindReplaceWidget extends Widget { }, 0); } + public showWithReplace(initialInput?: string, replaceInput?: string): void { + if (initialInput && !this._isVisible) { + this._findInput.setValue(initialInput); + } + + if (replaceInput && !this._isVisible) { + this._replaceInput.setValue(replaceInput); + } + + this._isVisible = true; + this._isReplaceVisible = true; + this._state.change({ isReplaceRevealed: this._isReplaceVisible }, false); + if (this._isReplaceVisible) { + this._innerReplaceDomNode.style.display = 'flex'; + } else { + this._innerReplaceDomNode.style.display = 'none'; + } + + setTimeout(() => { + dom.addClass(this._domNode, 'visible'); + dom.addClass(this._domNode, 'visible-transition'); + this._domNode.setAttribute('aria-hidden', 'false'); + this._updateButtons(); + + this._replaceInput.focus(); + }, 0); + } + public hide(): void { if (this._isVisible) { dom.removeClass(this._domNode, 'visible-transition'); diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index a2b8e7786f0..ec11e213ac4 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -6,8 +6,8 @@ import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import * as modes from 'vs/editor/common/modes'; -import { ActionsOrientation, ActionViewItem, ActionBar, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { Action, IActionRunner, IAction } from 'vs/base/common/actions'; +import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Action, IActionRunner, IAction, Separator } from 'vs/base/common/actions'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ITextModel } from 'vs/editor/common/model'; @@ -23,17 +23,17 @@ import { Emitter, Event } from 'vs/base/common/event'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdown'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { ToggleReactionsAction, ReactionAction, ReactionActionViewItem } from './reactionsAction'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget'; import { MenuItemAction, SubmenuItemAction, IMenu } from 'vs/platform/actions/common/actions'; -import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions'; import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; export class CommentNode extends Disposable { private _domNode: HTMLElement; @@ -79,7 +79,6 @@ export class CommentNode extends Disposable { @ICommentService private commentService: ICommentService, @IModelService private modelService: IModelService, @IModeService private modeService: IModeService, - @IKeybindingService private keybindingService: IKeybindingService, @INotificationService private notificationService: INotificationService, @IContextMenuService private contextMenuService: IContextMenuService, @IContextKeyService contextKeyService: IContextKeyService @@ -154,13 +153,12 @@ export class CommentNode extends Disposable { action, (action).menuActions, this.contextMenuService, - action => { - return this.actionViewItemProvider(action as Action); - }, - this.actionRunner!, - undefined, - 'toolbar-toggle-pickReactions codicon codicon-reactions', - () => { return AnchorAlignment.RIGHT; } + { + actionViewItemProvider: action => this.actionViewItemProvider(action as Action), + actionRunner: this.actionRunner, + classNames: ['toolbar-toggle-pickReactions', 'codicon', 'codicon-reactions'], + anchorAlignmentProvider: () => AnchorAlignment.RIGHT + } ); } return this.actionViewItemProvider(action as Action); @@ -221,8 +219,9 @@ export class CommentNode extends Disposable { let item = new ReactionActionViewItem(action); return item; } else if (action instanceof MenuItemAction) { - let item = new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); - return item; + return this.instantiationService.createInstance(MenuEntryActionViewItem, action); + } else if (action instanceof SubmenuItemAction) { + return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); } else { let item = new ActionViewItem({}, action, options); return item; @@ -259,16 +258,17 @@ export class CommentNode extends Disposable { toggleReactionAction, (toggleReactionAction).menuActions, this.contextMenuService, - action => { - if (action.id === ToggleReactionsAction.ID) { - return toggleReactionActionViewItem; - } - return this.actionViewItemProvider(action as Action); - }, - this.actionRunner!, - undefined, - 'toolbar-toggle-pickReactions', - () => { return AnchorAlignment.RIGHT; } + { + actionViewItemProvider: action => { + if (action.id === ToggleReactionsAction.ID) { + return toggleReactionActionViewItem; + } + return this.actionViewItemProvider(action as Action); + }, + actionRunner: this.actionRunner, + classNames: 'toolbar-toggle-pickReactions', + anchorAlignmentProvider: () => AnchorAlignment.RIGHT + } ); return toggleReactionAction; @@ -283,13 +283,12 @@ export class CommentNode extends Disposable { action, (action).menuActions, this.contextMenuService, - action => { - return this.actionViewItemProvider(action as Action); - }, - this.actionRunner!, - undefined, - 'toolbar-toggle-pickReactions', - () => { return AnchorAlignment.RIGHT; } + { + actionViewItemProvider: action => this.actionViewItemProvider(action as Action), + actionRunner: this.actionRunner, + classNames: 'toolbar-toggle-pickReactions', + anchorAlignmentProvider: () => AnchorAlignment.RIGHT + } ); } return this.actionViewItemProvider(action as Action); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 62c79e6c69c..d674f44afb6 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { ActionBar, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action, IAction } from 'vs/base/common/actions'; import * as arrays from 'vs/base/common/arrays'; import { Color } from 'vs/base/common/color'; @@ -26,13 +26,10 @@ import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer'; import { peekViewBorder } from 'vs/editor/contrib/peekView/peekView'; import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget'; import * as nls from 'vs/nls'; -import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenu, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKey, 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 { INotificationService } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { contrastBorder, editorForeground, focusBorder, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, transparent } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; @@ -49,6 +46,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; const COLLAPSE_ACTION_CLASS = 'expand-review-action codicon-chevron-up'; @@ -109,15 +107,12 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget private _owner: string, private _commentThread: modes.CommentThread, private _pendingComment: string | null, - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService private instantiationService: IInstantiationService, @IModeService private modeService: IModeService, @IModelService private modelService: IModelService, @IThemeService private themeService: IThemeService, @ICommentService private commentService: ICommentService, @IOpenerService private openerService: IOpenerService, - @IKeybindingService private keybindingService: IKeybindingService, - @INotificationService private notificationService: INotificationService, - @IContextMenuService private contextMenuService: IContextMenuService, @IContextKeyService contextKeyService: IContextKeyService ) { super(editor, { keepEditorSelection: true }); @@ -239,11 +234,11 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._actionbarWidget = new ActionBar(actionsContainer, { actionViewItemProvider: (action: IAction) => { if (action instanceof MenuItemAction) { - let item = new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); - return item; + return this.instantiationService.createInstance(MenuEntryActionViewItem, action); + } else if (action instanceof SubmenuItemAction) { + return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); } else { - let item = new ActionViewItem({}, action, { label: false, icon: true }); - return item; + return new ActionViewItem({}, action, { label: false, icon: true }); } } }); diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index c6e976913b5..ed8b5735426 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ContextSubMenu } from 'vs/base/browser/contextmenu'; import { $ } from 'vs/base/browser/dom'; import { Action, IAction } from 'vs/base/common/actions'; import { coalesce, findFirstInSorted } from 'vs/base/common/arrays'; @@ -547,8 +546,8 @@ export class CommentController implements IEditorContribution { return picks; } - private getContextMenuActions(commentInfos: { ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: modes.CommentingRanges }[], lineNumber: number): (IAction | ContextSubMenu)[] { - const actions: (IAction | ContextSubMenu)[] = []; + private getContextMenuActions(commentInfos: { ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: modes.CommentingRanges }[], lineNumber: number): IAction[] { + const actions: IAction[] = []; commentInfos.forEach(commentInfo => { const { ownerId, extensionId, label } = commentInfo; diff --git a/src/vs/workbench/contrib/comments/browser/reactionsAction.ts b/src/vs/workbench/contrib/comments/browser/reactionsAction.ts index a5ae07ec17f..ef091f4dd7b 100644 --- a/src/vs/workbench/contrib/comments/browser/reactionsAction.ts +++ b/src/vs/workbench/contrib/comments/browser/reactionsAction.ts @@ -5,9 +5,9 @@ import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; -import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action, IAction } from 'vs/base/common/actions'; import { URI, UriComponents } from 'vs/base/common/uri'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; export class ToggleReactionsAction extends Action { static readonly ID = 'toolbar.toggle.pickReactions'; diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 56d2bd17c8b..c2bbe5e9a35 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -8,7 +8,7 @@ import * as env from 'vs/base/common/platform'; import * as dom from 'vs/base/browser/dom'; import { URI } from 'vs/base/common/uri'; import severity from 'vs/base/common/severity'; -import { IAction, Action } from 'vs/base/common/actions'; +import { IAction, Action, SubmenuAction } from 'vs/base/common/actions'; import { Range } from 'vs/editor/common/core/range'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType, IContentWidget, IActiveCodeEditor, IContentWidgetPosition, ContentWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness, ITextModel, OverviewRulerLane, IModelDecorationOverviewRulerOptions } from 'vs/editor/common/model'; @@ -18,7 +18,6 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { RemoveBreakpointAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, BreakpointWidgetContext, IBreakpointEditorContribution, IBreakpointUpdateData, IDebugConfiguration, State, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; -import { ContextSubMenu } from 'vs/base/browser/contextmenu'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { BreakpointWidget } from 'vs/workbench/contrib/debug/browser/breakpointWidget'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -33,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'; @@ -88,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 }; } @@ -288,8 +287,8 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi })); } - private getContextMenuActions(breakpoints: ReadonlyArray, uri: URI, lineNumber: number, column?: number): Array { - const actions: Array = []; + private getContextMenuActions(breakpoints: ReadonlyArray, uri: URI, lineNumber: number, column?: number): IAction[] { + const actions: IAction[] = []; if (breakpoints.length === 1) { const breakpointType = breakpoints[0].logMessage ? nls.localize('logPoint', "Logpoint") : nls.localize('breakpoint', "Breakpoint"); actions.push(new RemoveBreakpointAction(RemoveBreakpointAction.ID, nls.localize('removeBreakpoint', "Remove {0}", breakpointType), this.debugService)); @@ -310,7 +309,7 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi )); } else if (breakpoints.length > 1) { const sorted = breakpoints.slice().sort((first, second) => (first.column && second.column) ? first.column - second.column : 1); - actions.push(new ContextSubMenu(nls.localize('removeBreakpoints', "Remove Breakpoints"), sorted.map(bp => new Action( + actions.push(new SubmenuAction('debug.removeBreakpoints', nls.localize('removeBreakpoints', "Remove Breakpoints"), sorted.map(bp => new Action( 'removeInlineBreakpoint', bp.column ? nls.localize('removeInlineBreakpointOnColumn', "Remove Inline Breakpoint on Column {0}", bp.column) : nls.localize('removeLineBreakpoint', "Remove Line Breakpoint"), undefined, @@ -318,7 +317,7 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi () => this.debugService.removeBreakpoints(bp.getId()) )))); - actions.push(new ContextSubMenu(nls.localize('editBreakpoints', "Edit Breakpoints"), sorted.map(bp => + actions.push(new SubmenuAction('debug.editBReakpoints', nls.localize('editBreakpoints', "Edit Breakpoints"), sorted.map(bp => new Action('editBreakpoint', bp.column ? nls.localize('editInlineBreakpointOnColumn', "Edit Inline Breakpoint on Column {0}", bp.column) : nls.localize('editLineBrekapoint', "Edit Line Breakpoint"), undefined, @@ -327,7 +326,7 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi ) ))); - actions.push(new ContextSubMenu(nls.localize('enableDisableBreakpoints', "Enable/Disable Breakpoints"), sorted.map(bp => new Action( + actions.push(new SubmenuAction('debug.enableDisableBreakpoints', nls.localize('enableDisableBreakpoints', "Enable/Disable Breakpoints"), sorted.map(bp => new Action( bp.enabled ? 'disableColumnBreakpoint' : 'enableColumnBreakpoint', bp.enabled ? (bp.column ? nls.localize('disableInlineColumnBreakpoint', "Disable Inline Breakpoint on Column {0}", bp.column) : nls.localize('disableBreakpointOnLine', "Disable Line Breakpoint")) : (bp.column ? nls.localize('enableBreakpoints', "Enable Inline Breakpoint on Column {0}", bp.column) : nls.localize('enableBreakpointOnLine', "Enable Line Breakpoint")), @@ -548,7 +547,7 @@ class InlineBreakpointWidget implements IContentWidget, IDisposable { private readonly breakpoint: IBreakpoint | undefined, private readonly debugService: IDebugService, private readonly contextMenuService: IContextMenuService, - private readonly getContextMenuActions: () => ReadonlyArray + private readonly getContextMenuActions: () => IAction[] ) { this.range = this.editor.getModel().getDecorationRange(decorationId); this.toDispose.push(this.editor.onDidChangeModelDecorations(() => { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index c0164a0d939..e2138294a00 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import * as resources from 'vs/base/common/resources'; import * as dom from 'vs/base/browser/dom'; -import { IAction, Action } from 'vs/base/common/actions'; +import { IAction, Action, Separator } from 'vs/base/common/actions'; import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINTS_FOCUSED, State, DEBUG_SCHEME, IFunctionBreakpoint, IExceptionBreakpoint, IEnablement, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, IDebugModel, IDataBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; import { ExceptionBreakpoint, FunctionBreakpoint, Breakpoint, DataBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { AddFunctionBreakpointAction, ToggleBreakpointsActivatedAction, RemoveAllBreakpointsAction, RemoveBreakpointAction, EnableAllBreakpointsAction, DisableAllBreakpointsAction, ReapplyBreakpointsAction } from 'vs/workbench/contrib/debug/browser/debugActions'; @@ -16,7 +16,6 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Constants } from 'vs/base/common/uint'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IListVirtualDelegate, IListContextMenuEvent, IListRenderer } from 'vs/base/browser/ui/list/list'; import { IEditorPane } from 'vs/workbench/common/editor'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 13e0d80e91a..96b2a9827d9 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -11,7 +11,7 @@ import { IDebugService, State, IStackFrame, IDebugSession, IThread, CONTEXT_CALL import { Thread, StackFrame, ThreadAndSessionIds } from 'vs/workbench/contrib/debug/common/debugModel'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { MenuId, IMenu, IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { MenuId, IMenu, IMenuService, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { IAction, Action } from 'vs/base/common/actions'; @@ -21,7 +21,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { ILabelService } from 'vs/platform/label/common/label'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { createAndFillInContextMenuActions, createAndFillInActionBarActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { createAndFillInContextMenuActions, createAndFillInActionBarActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeNode, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { WorkbenchCompressibleAsyncDataTree } from 'vs/platform/list/browser/listService'; @@ -449,7 +449,6 @@ export class CallStackView extends ViewPane { interface IThreadTemplateData { thread: HTMLElement; name: HTMLElement; - state: HTMLElement; stateLabel: HTMLSpanElement; label: HighlightedLabel; actionBar: ActionBar; @@ -458,7 +457,6 @@ interface IThreadTemplateData { interface ISessionTemplateData { session: HTMLElement; name: HTMLElement; - state: HTMLElement; stateLabel: HTMLSpanElement; label: HighlightedLabel; actionBar: ActionBar; @@ -489,10 +487,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer { if (action instanceof MenuItemAction) { - // We need the MenuEntryActionViewItem so the icon would get rendered - return new MenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + return this.instantiationService.createInstance(MenuEntryActionViewItem, action); + } else if (action instanceof SubmenuItemAction) { + return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); } return undefined; } }); - return { session, name, state, stateLabel, label, actionBar, elementDisposable: [] }; + return { session, name, stateLabel, label, actionBar, elementDisposable: [] }; } renderElement(element: ITreeNode, _: number, data: ISessionTemplateData): void { @@ -583,12 +578,11 @@ class ThreadsRenderer implements ICompressibleTreeRenderer, index: number, data: IThreadTemplateData): void { diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 20150caa071..abe68683a9c 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -229,7 +229,7 @@ configurationRegistry.registerConfiguration({ }, 'debug.openDebug': { enum: ['neverOpen', 'openOnSessionStart', 'openOnFirstSessionStart', 'openOnDebugBreak'], - default: 'openOnSessionStart', + default: 'openOnFirstSessionStart', description: nls.localize('openDebug', "Controls when the debug view should open.") }, 'debug.showSubSessionsInToolBar': { @@ -646,10 +646,10 @@ registerThemingParticipant((theme, collector) => { /* 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 { + .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}; } diff --git a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts index 4e0766cb2a5..4b3c2a72a1c 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts @@ -4,12 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { IAction, IActionRunner } from 'vs/base/common/actions'; +import { IAction, IActionRunner, IActionViewItem } from 'vs/base/common/actions'; import { KeyCode } from 'vs/base/common/keyCodes'; import * as dom from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { SelectBox, ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox'; -import { SelectActionViewItem, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IDebugService, IDebugSession, IDebugConfiguration, IConfig, ILaunch } from 'vs/workbench/contrib/debug/common/debug'; @@ -20,6 +19,7 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ADD_CONFIGURATION_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; +import { SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; const $ = dom.$; diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 55abf0ca1cd..5b95e4ce9c3 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -270,7 +270,9 @@ export class DebugService implements IDebugService { try { // make sure to save all files and that the configuration is up to date await this.extensionService.activateByEvent('onDebug'); - await this.editorService.saveAll(); + if (!options?.parentSession) { + await this.editorService.saveAll(); + } await this.configurationService.reloadConfiguration(launch ? launch.workspace : undefined); await this.extensionService.whenInstalledExtensionsRegistered(); @@ -410,6 +412,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". @@ -434,12 +442,7 @@ 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.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); @@ -561,8 +564,11 @@ export class DebugService implements IDebugService { this.toDispose.push(session.onDidEndAdapter(async adapterExitEvent => { - if (adapterExitEvent.error) { - this.notificationService.error(nls.localize('debugAdapterCrash', "Debug adapter process has terminated unexpectedly ({0})", adapterExitEvent.error.message || adapterExitEvent.error.toString())); + if (adapterExitEvent) { + if (adapterExitEvent.error) { + this.notificationService.error(nls.localize('debugAdapterCrash', "Debug adapter process has terminated unexpectedly ({0})", adapterExitEvent.error.message || adapterExitEvent.error.toString())); + } + this.telemetry.logDebugSessionStop(session, adapterExitEvent); } // 'Run without debugging' mode VSCode must terminate the extension host. More details: #3905 @@ -571,8 +577,6 @@ export class DebugService implements IDebugService { this.extensionHostDebugService.close(extensionDebugSession.getId()); } - this.telemetry.logDebugSessionStop(session, adapterExitEvent); - if (session.configuration.postDebugTask) { try { await this.taskRunner.runTask(session.root, session.configuration.postDebugTask); @@ -817,6 +821,7 @@ export class DebugService implements IDebugService { async enableOrDisableBreakpoints(enable: boolean, breakpoint?: IEnablement): Promise { if (breakpoint) { this.model.setEnablement(breakpoint, enable); + this.debugStorage.storeBreakpoints(this.model); if (breakpoint instanceof Breakpoint) { await this.sendBreakpoints(breakpoint.uri); } else if (breakpoint instanceof FunctionBreakpoint) { @@ -828,6 +833,7 @@ export class DebugService implements IDebugService { } } else { this.model.enableOrDisableAllBreakpoints(enable); + this.debugStorage.storeBreakpoints(this.model); await this.sendAllBreakpoints(); } this.debugStorage.storeBreakpoints(this.model); @@ -838,6 +844,9 @@ export class DebugService implements IDebugService { breakpoints.forEach(bp => aria.status(nls.localize('breakpointAdded', "Added breakpoint, line {0}, file {1}", bp.lineNumber, uri.fsPath))); breakpoints.forEach(bp => this.telemetry.logDebugAddBreakpoint(bp, context)); + // In some cases we need to store breakpoints before we send them because sending them can take a long time + // And after sending them because the debug adapter can attach adapter data to a breakpoint + this.debugStorage.storeBreakpoints(this.model); await this.sendBreakpoints(uri); this.debugStorage.storeBreakpoints(this.model); return breakpoints; @@ -845,12 +854,13 @@ export class DebugService implements IDebugService { async updateBreakpoints(uri: uri, data: Map, sendOnResourceSaved: boolean): Promise { this.model.updateBreakpoints(data); + this.debugStorage.storeBreakpoints(this.model); if (sendOnResourceSaved) { this.breakpointsToSendOnResourceSaved.add(uri.toString()); } else { await this.sendBreakpoints(uri); + this.debugStorage.storeBreakpoints(this.model); } - this.debugStorage.storeBreakpoints(this.model); } async removeBreakpoints(id?: string): Promise { @@ -860,8 +870,8 @@ export class DebugService implements IDebugService { this.model.removeBreakpoints(toRemove); - await Promise.all(urisToClear.map(uri => this.sendBreakpoints(uri))); this.debugStorage.storeBreakpoints(this.model); + await Promise.all(urisToClear.map(uri => this.sendBreakpoints(uri))); } setBreakpointsActivated(activated: boolean): Promise { @@ -876,27 +886,27 @@ export class DebugService implements IDebugService { async renameFunctionBreakpoint(id: string, newFunctionName: string): Promise { this.model.renameFunctionBreakpoint(id, newFunctionName); - await this.sendFunctionBreakpoints(); this.debugStorage.storeBreakpoints(this.model); + await this.sendFunctionBreakpoints(); } async removeFunctionBreakpoints(id?: string): Promise { this.model.removeFunctionBreakpoints(id); - await this.sendFunctionBreakpoints(); this.debugStorage.storeBreakpoints(this.model); + await this.sendFunctionBreakpoints(); } async addDataBreakpoint(label: string, dataId: string, canPersist: boolean, accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined): Promise { this.model.addDataBreakpoint(label, dataId, canPersist, accessTypes); + this.debugStorage.storeBreakpoints(this.model); await this.sendDataBreakpoints(); - this.debugStorage.storeBreakpoints(this.model); } async removeDataBreakpoints(id?: string): Promise { this.model.removeDataBreakpoints(id); - await this.sendDataBreakpoints(); this.debugStorage.storeBreakpoints(this.model); + await this.sendDataBreakpoints(); } async sendAllBreakpoints(session?: IDebugSession): Promise { @@ -909,7 +919,6 @@ export class DebugService implements IDebugService { private async sendBreakpoints(modelUri: uri, sourceModified = false, session?: IDebugSession): Promise { const breakpointsToSend = this.model.getBreakpoints({ uri: modelUri, enabledOnly: true }); - await sendToOneOrAllSessions(this.model, session, s => s.sendBreakpoints(modelUri, breakpointsToSend, sourceModified)); } @@ -923,10 +932,10 @@ export class DebugService implements IDebugService { }); } - private sendDataBreakpoints(session?: IDebugSession): Promise { + private async sendDataBreakpoints(session?: IDebugSession): Promise { const breakpointsToSend = this.model.getDataBreakpoints().filter(fbp => fbp.enabled && this.model.areBreakpointsActivated()); - return sendToOneOrAllSessions(this.model, session, async s => { + await sendToOneOrAllSessions(this.model, session, async s => { if (s.capabilities.supportsDataBreakpoints) { await s.sendDataBreakpoints(breakpointsToSend); } diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index f6d4e37cbb8..cd67a54853f 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -36,6 +36,7 @@ import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { localize } from 'vs/nls'; import { canceled } from 'vs/base/common/errors'; import { filterExceptionsFromTelemetry } from 'vs/workbench/contrib/debug/common/debugUtils'; +import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; export class DebugSession implements IDebugSession { @@ -52,7 +53,7 @@ export class DebugSession implements IDebugSession { private repl: ReplModel; private readonly _onDidChangeState = new Emitter(); - private readonly _onDidEndAdapter = new Emitter(); + private readonly _onDidEndAdapter = new Emitter(); private readonly _onDidLoadedSource = new Emitter(); private readonly _onDidCustomEvent = new Emitter(); @@ -133,6 +134,10 @@ export class DebugSession implements IDebugSession { return !!this._options.compact; } + get compoundRoot(): DebugCompoundRoot | undefined { + return this._options.compoundRoot; + } + setConfiguration(configuration: { resolved: IConfig, unresolved: IConfig | undefined }) { this._configuration = configuration; } @@ -176,7 +181,7 @@ export class DebugSession implements IDebugSession { return this._onDidChangeState.event; } - get onDidEndAdapter(): Event { + get onDidEndAdapter(): Event { return this._onDidEndAdapter.event; } @@ -280,14 +285,17 @@ export class DebugSession implements IDebugSession { */ async terminate(restart = false): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'terminate')); + // Adapter went down but it did not send a 'terminated' event, simulate like the event has been sent + this.onDidExitAdapter(); } this.cancelAllRequests(); - if (this.raw.capabilities.supportsTerminateRequest && this._configuration.resolved.request === 'launch') { - await this.raw.terminate(restart); - } else { - await this.raw.disconnect(restart); + if (this.raw) { + if (this.raw.capabilities.supportsTerminateRequest && this._configuration.resolved.request === 'launch') { + await this.raw.terminate(restart); + } else { + await this.raw.disconnect(restart); + } } if (!restart) { @@ -300,11 +308,14 @@ export class DebugSession implements IDebugSession { */ async disconnect(restart = false): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'disconnect')); + // Adapter went down but it did not send a 'terminated' event, simulate like the event has been sent + this.onDidExitAdapter(); } this.cancelAllRequests(); - await this.raw.disconnect(restart); + if (this.raw) { + await this.raw.disconnect(restart); + } if (!restart) { this._options.compoundRoot?.sessionStopped(); @@ -984,12 +995,14 @@ export class DebugSession implements IDebugSession { this._onDidProgressEnd.fire(event); })); - this.rawListeners.push(this.raw.onDidExitAdapter(event => { - this.initialized = true; - this.model.setBreakpointSessionData(this.getId(), this.capabilities, undefined); - this.shutdown(); - this._onDidEndAdapter.fire(event); - })); + this.rawListeners.push(this.raw.onDidExitAdapter(event => this.onDidExitAdapter(event))); + } + + private onDidExitAdapter(event?: AdapterEndEvent): void { + this.initialized = true; + this.model.setBreakpointSessionData(this.getId(), this.capabilities, undefined); + this.shutdown(); + this._onDidEndAdapter.fire(event); } // Disconnects and clears state. Session can be initialized again for a new connection. diff --git a/src/vs/workbench/contrib/debug/browser/debugStatus.ts b/src/vs/workbench/contrib/debug/browser/debugStatus.ts index e1736d44af7..a83612abdca 100644 --- a/src/vs/workbench/contrib/debug/browser/debugStatus.ts +++ b/src/vs/workbench/contrib/debug/browser/debugStatus.ts @@ -65,7 +65,7 @@ export class DebugStatusContribution implements IWorkbenchContribution { } return { - text: '$(play) ' + text, + text: '$(debug-alt-small) ' + text, ariaLabel: nls.localize('debugTarget', "Debug: {0}", text), tooltip: nls.localize('selectAndStartDebug', "Select and start debug configuration"), command: 'workbench.action.debug.selectandstart' diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 0b082ed02bf..54c7593b579 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -9,8 +9,8 @@ import * as browser from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; import * as arrays from 'vs/base/common/arrays'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IAction, IRunEvent, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; -import { ActionBar, ActionsOrientation, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IAction, IRunEvent, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, Separator } from 'vs/base/common/actions'; +import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IDebugConfiguration, IDebugService, State } from 'vs/workbench/contrib/debug/common/debug'; @@ -21,13 +21,12 @@ 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 { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; 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'; -import { createAndFillInActionBarActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenu, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { createAndFillInActionBarActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenu, IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { FocusSessionAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -57,7 +56,6 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { @IStorageService private readonly storageService: IStorageService, @IConfigurationService private readonly configurationService: IConfigurationService, @IThemeService themeService: IThemeService, - @IKeybindingService private readonly keybindingService: IKeybindingService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IMenuService menuService: IMenuService, @IContextMenuService contextMenuService: IContextMenuService, @@ -80,9 +78,10 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { actionViewItemProvider: (action: IAction) => { if (action.id === FocusSessionAction.ID) { return this.instantiationService.createInstance(FocusSessionActionViewItem, action); - } - if (action instanceof MenuItemAction) { - return new MenuEntryActionViewItem(action, this.keybindingService, this.notificationService, contextMenuService); + } else if (action instanceof MenuItemAction) { + return this.instantiationService.createInstance(MenuEntryActionViewItem, action); + } else if (action instanceof SubmenuItemAction) { + return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); } return undefined; diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index ca458d7b809..bbe7ac03099 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -5,9 +5,8 @@ import 'vs/css!./media/debugViewlet'; import * as nls from 'vs/nls'; -import { IAction } from 'vs/base/common/actions'; +import { IAction, IActionViewItem } from 'vs/base/common/actions'; import * as DOM from 'vs/base/browser/dom'; -import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IDebugService, VIEWLET_ID, State, BREAKPOINTS_VIEW_ID, IDebugConfiguration, CONTEXT_DEBUG_UX, CONTEXT_DEBUG_UX_KEY, REPL_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; import { StartAction, ConfigureAction, SelectAndStartAction, FocusSessionAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { StartDebugActionViewItem, FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; @@ -24,15 +23,14 @@ import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/la import { memoize } from 'vs/base/common/decorators'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { DebugToolBar } from 'vs/workbench/contrib/debug/browser/debugToolBar'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ViewPane, ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { IMenu, MenuId, IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IMenu, MenuId, IMenuService, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; 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'; export class DebugViewPaneContainer extends ViewPaneContainer { @@ -42,6 +40,7 @@ export class DebugViewPaneContainer extends ViewPaneContainer { private paneListeners = new Map(); private debugToolBarMenu: IMenu | undefined; private disposeOnTitleUpdate: IDisposable | undefined; + private updateToolBarScheduler: RunOnceScheduler; constructor( @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @@ -55,17 +54,24 @@ export class DebugViewPaneContainer extends ViewPaneContainer { @IContextMenuService contextMenuService: IContextMenuService, @IExtensionService extensionService: IExtensionService, @IConfigurationService configurationService: IConfigurationService, - @IKeybindingService private readonly keybindingService: IKeybindingService, @IContextViewService private readonly contextViewService: IContextViewService, @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, - @INotificationService private readonly notificationService: INotificationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService ) { super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); + this.updateToolBarScheduler = this._register(new RunOnceScheduler(() => { + if (this.configurationService.getValue('debug').toolBarLocation === 'docked') { + this.updateTitleArea(); + } + }, 20)); + + // When there are potential updates to the docked debug toolbar we need to update it this._register(this.debugService.onDidChangeState(state => this.onDebugServiceStateChange(state))); - this._register(this.debugService.onDidNewSession(() => this.updateToolBar())); + this._register(this.debugService.onDidNewSession(() => this.updateToolBarScheduler.schedule())); + this._register(this.debugService.getViewModel().onDidFocusSession(() => this.updateToolBarScheduler.schedule())); + this._register(this.contextKeyService.onDidChangeContext(e => { if (e.affectsSome(new Set([CONTEXT_DEBUG_UX_KEY]))) { this.updateTitleArea(); @@ -125,6 +131,7 @@ export class DebugViewPaneContainer extends ViewPaneContainer { if (!this.debugToolBarMenu) { this.debugToolBarMenu = this.menuService.createMenu(MenuId.DebugToolBar, this.contextKeyService); this._register(this.debugToolBarMenu); + this._register(this.debugToolBarMenu.onDidChange(() => this.updateToolBarScheduler.schedule())); } const { actions, disposable } = DebugToolBar.getActions(this.debugToolBarMenu, this.debugService, this.instantiationService); @@ -165,7 +172,9 @@ export class DebugViewPaneContainer extends ViewPaneContainer { return new FocusSessionActionViewItem(action, this.debugService, this.themeService, this.contextViewService, this.configurationService); } if (action instanceof MenuItemAction) { - return new MenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + return this.instantiationService.createInstance(MenuEntryActionViewItem, action); + } else if (action instanceof SubmenuItemAction) { + return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); } return undefined; @@ -190,13 +199,7 @@ export class DebugViewPaneContainer extends ViewPaneContainer { }); } - this.updateToolBar(); - } - - private updateToolBar(): void { - if (this.configurationService.getValue('debug').toolBarLocation === 'docked') { - this.updateTitleArea(); - } + this.updateToolBarScheduler.schedule(); } addPanes(panes: { pane: ViewPane, size: number, index?: number }[]): void { diff --git a/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css b/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css index 70847c6647a..1889028b46b 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css @@ -38,7 +38,7 @@ cursor: grabbing; } -.monaco-workbench .debug-toolbar .monaco-action-bar .action-item > .action-label { +.monaco-workbench .debug-toolbar .monaco-action-bar .action-item .action-label { width: 32px; height: 32px; margin-right: 0; diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index 3ab3a5c63ef..b7e09375af9 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -78,8 +78,8 @@ } /* Make icons and text the same color as the list foreground on focus selection */ -.debug-pane .monaco-list:focus .monaco-list-row.selected .state > .label, -.debug-pane .monaco-list:focus .monaco-list-row.selected.focused .state > .label, +.debug-pane .monaco-list:focus .monaco-list-row.selected .state.label, +.debug-pane .monaco-list:focus .monaco-list-row.selected.focused .state.label, .debug-pane .monaco-list:focus .monaco-list-row.selected .codicon, .debug-pane .monaco-list:focus .monaco-list-row.selected.focused .codicon { color: inherit !important; @@ -120,18 +120,17 @@ text-overflow: ellipsis; } -.debug-pane .debug-call-stack .thread > .state, -.debug-pane .debug-call-stack .session > .state { - display: flex; - align-items: center; - text-align: right; +.debug-pane .debug-call-stack .thread > .state.label, +.debug-pane .debug-call-stack .session > .state.label { overflow: hidden; text-overflow: ellipsis; - padding: 0 10px; + margin: 0 10px; text-transform: uppercase; + align-self: center; + font-size: 0.8em; } -.debug-pane .debug-call-stack .monaco-list-row:hover .state { +.debug-pane .debug-call-stack .monaco-list-row:hover .state.label { display: none; } @@ -164,12 +163,6 @@ background-repeat: no-repeat; } -.debug-pane .debug-call-stack .thread > .state > .label, -.debug-pane .debug-call-stack .session > .state > .label { - font-size: 0.8em; - min-height: auto; -} - .debug-pane .debug-call-stack .stack-frame { overflow: hidden; text-overflow: ellipsis; diff --git a/src/vs/workbench/contrib/debug/browser/media/repl.css b/src/vs/workbench/contrib/debug/browser/media/repl.css index 1970134e9c3..b9599c28628 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, diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 326bfb8c80c..6afa8ef0e4f 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/repl'; import { URI as uri } from 'vs/base/common/uri'; -import { IAction, IActionViewItem, Action } from 'vs/base/common/actions'; +import { IAction, IActionViewItem, Action, Separator } from 'vs/base/common/actions'; import * as dom from 'vs/base/browser/dom'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -41,7 +41,6 @@ import { first } from 'vs/base/common/arrays'; import { ITreeNode, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; -import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { removeAnsiEscapeCodes } from 'vs/base/common/strings'; import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; @@ -440,7 +439,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { return this.instantiationService.createInstance(SelectReplActionViewItem, this.selectReplAction); } - return undefined; + return super.getActionViewItem(action); } getActions(): IAction[] { diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index de19118f1b2..16b982c463a 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -13,9 +13,8 @@ import { Variable, Scope, ErrorScope, StackFrame } from 'vs/workbench/contrib/de import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { renderViewTree, renderVariable, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView'; -import { IAction, Action } from 'vs/base/common/actions'; +import { IAction, Action, Separator } from 'vs/base/common/actions'; import { CopyValueAction } from 'vs/workbench/contrib/debug/browser/debugActions'; -import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index 724ad7e72d8..9cf18480f2b 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -14,8 +14,7 @@ import { AddWatchExpressionAction, RemoveAllWatchExpressionsAction, CopyValueAct 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 { IAction, Action } from 'vs/base/common/actions'; -import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IAction, Action, Separator } from 'vs/base/common/actions'; import { renderExpressionValue, renderViewTree, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 620831e332c..d74d8cce52f 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -169,6 +169,7 @@ export interface IDebugSession extends ITreeElement { readonly parentSession: IDebugSession | undefined; readonly subId: string | undefined; readonly compact: boolean; + readonly compoundRoot: DebugCompoundRoot | undefined; setSubId(subId: string | undefined): void; @@ -194,7 +195,7 @@ export interface IDebugSession extends ITreeElement { logToRepl(sev: severity, args: any[], frame?: { uri: uri, line: number, column: number }): void; // session events - readonly onDidEndAdapter: Event; + readonly onDidEndAdapter: Event; readonly onDidChangeState: Event; readonly onDidChangeReplElements: Event; diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index c0d9595c1c0..1a8a5879ca6 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -689,7 +689,7 @@ export class Breakpoint extends BaseBreakpoint implements IBreakpoint { toJSON(): any { const result = super.toJSON(); - result.uri = this.uri; + result.uri = this._uri; result.lineNumber = this._lineNumber; result.column = this._column; result.adapterData = this.adapterData; diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 1b3b1f2beba..7e9b4377b82 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -13,6 +13,7 @@ import Severity from 'vs/base/common/severity'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; 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'; export class MockDebugService implements IDebugService { @@ -135,6 +136,10 @@ export class MockDebugService implements IDebugService { } export class MockSession implements IDebugSession { + get compoundRoot(): DebugCompoundRoot | undefined { + return undefined; + } + stepInTargets(frameId: number): Promise<{ id: number; label: string; }[]> { throw new Error('Method not implemented.'); } @@ -227,7 +232,7 @@ export class MockSession implements IDebugSession { throw new Error('not implemented'); } - get onDidEndAdapter(): Event { + get onDidEndAdapter(): Event { throw new Error('not implemented'); } diff --git a/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts index b9cc0f9941e..8215282b35b 100644 --- a/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts @@ -21,8 +21,13 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations { private importantTips: IConfigBasedExtensionTip[] = []; private otherTips: IConfigBasedExtensionTip[] = []; - private _recommendations: ExtensionRecommendation[] = []; - get recommendations(): ReadonlyArray { return this._recommendations; } + private _otherRecommendations: ExtensionRecommendation[] = []; + get otherRecommendations(): ReadonlyArray { return this._otherRecommendations; } + + private _importantRecommendations: ExtensionRecommendation[] = []; + get importantRecommendations(): ReadonlyArray { return this._importantRecommendations; } + + get recommendations(): ReadonlyArray { return [...this.importantRecommendations, ...this.otherRecommendations]; } constructor( isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, @@ -61,7 +66,8 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations { } this.importantTips = [...importantTips.values()]; this.otherTips = [...otherTips.values()].filter(tip => !importantTips.has(tip.extensionId)); - this._recommendations = [...this.importantTips, ...this.otherTips].map(tip => this.toExtensionRecommendation(tip)); + this._otherRecommendations = this.otherTips.map(tip => this.toExtensionRecommendation(tip)); + this._importantRecommendations = this.importantTips.map(tip => this.toExtensionRecommendation(tip)); } private async promptWorkspaceRecommendations(): Promise { @@ -88,7 +94,7 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations { 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.promptImportantExtensionInstallNotification(extension, message); + this.promptImportantExtensionsInstallNotification([extension], message); } } diff --git a/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts index 1911eb12497..60a16d6fc88 100644 --- a/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts @@ -9,13 +9,14 @@ import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/ import { timeout } from 'vs/base/common/async'; import { localize } from 'vs/nls'; import { IStringDictionary } from 'vs/base/common/collections'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { basename } from 'vs/base/common/path'; import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; 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 { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; type ExeExtensionRecommendationsClassification = { extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' }; @@ -24,13 +25,22 @@ type ExeExtensionRecommendationsClassification = { export class ExeBasedRecommendations extends ExtensionRecommendations { - readonly _recommendations: ExtensionRecommendation[] = []; - get recommendations(): ReadonlyArray { return this._recommendations; } + + private readonly _otherRecommendations: ExtensionRecommendation[] = []; + get otherRecommendations(): ReadonlyArray { return this._otherRecommendations; } + + private readonly _importantRecommendations: ExtensionRecommendation[] = []; + get importantRecommendations(): ReadonlyArray { return this._importantRecommendations; } + + get recommendations(): ReadonlyArray { return [...this.importantRecommendations, ...this.otherRecommendations]; } + + private readonly tasExperimentService: ITASExperimentService | undefined; constructor( isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, @IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @optional(ITASExperimentService) tasExperimentService: ITASExperimentService, @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @INotificationService notificationService: INotificationService, @@ -39,6 +49,7 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + this.tasExperimentService = tasExperimentService; /* 3s has come out to be the good number to fetch and prompt important exe based recommendations @@ -49,16 +60,30 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { protected async doActivate(): Promise { const otherExectuableBasedTips = await this.extensionTipsService.getOtherExecutableBasedTips(); - otherExectuableBasedTips.forEach(tip => this._recommendations.push(this.toExtensionRecommendation(tip))); + otherExectuableBasedTips.forEach(tip => this._otherRecommendations.push(this.toExtensionRecommendation(tip))); + await this.fetchImportantExeBasedRecommendations(); } - private async fetchAndPromptImportantExeBasedRecommendations(): 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._recommendations.push(this.toExtensionRecommendation(tip)); + this._importantRecommendations.push(this.toExtensionRecommendation(tip)); importantExeBasedRecommendations[tip.extensionId.toLowerCase()] = tip; }); + return importantExeBasedRecommendations; + } + + private async fetchAndPromptImportantExeBasedRecommendations(): Promise { + const importantExeBasedRecommendations = await this.fetchImportantExeBasedRecommendations(); const local = await this.extensionManagementService.getInstalled(); const { installed, uninstalled } = this.groupByInstalled(Object.keys(importantExeBasedRecommendations), local); @@ -76,7 +101,7 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { this.promptImportantExeBasedRecommendations(uninstalled, importantExeBasedRecommendations); } - private promptImportantExeBasedRecommendations(recommendations: string[], importantExeBasedRecommendations: IStringDictionary): void { + private async promptImportantExeBasedRecommendations(recommendations: string[], importantExeBasedRecommendations: IStringDictionary): Promise { if (this.hasToIgnoreRecommendationNotifications()) { return; } @@ -85,11 +110,39 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { return; } + const recommendationsByExe = new Map(); for (const extensionId of recommendations) { const tip = importantExeBasedRecommendations[extensionId]; - 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.promptImportantExtensionInstallNotification(extensionId, message); + let tips = recommendationsByExe.get(tip.exeFriendlyName); + if (!tips) { + tips = []; + recommendationsByExe.set(tip.exeFriendlyName, tips); + } + tips.push(tip); + } + + for (const [, tips] of recommendationsByExe) { + const extensionIds = tips.map(({ extensionId }) => extensionId.toLowerCase()); + if (this.tasExperimentService && extensionIds.indexOf('ms-vscode-remote.remote-wsl') !== -1) { + 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); + } } } @@ -112,7 +165,7 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { source: 'executable', reason: { reasonId: ExtensionRecommendationReason.Executable, - reasonText: localize('exeBasedRecommendation', "This extension is recommended because you have {0} installed.", tip.extensionName) + reasonText: localize('exeBasedRecommendation', "This extension is recommended because you have {0} installed.", tip.exeFriendlyName || basename(tip.windowsPath!)) } }; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts index 1001b15bfd4..4347c796632 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts @@ -8,7 +8,7 @@ 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 } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { InstallRecommendedExtensionAction, ShowRecommendedExtensionAction, ShowRecommendedExtensionsAction, InstallRecommendedExtensionsAction } 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 { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; @@ -65,26 +65,40 @@ export abstract class ExtensionRecommendations extends Disposable { } } - protected promptImportantExtensionInstallNotification(extensionId: string, message: string): void { + protected promptImportantExtensionsInstallNotification(extensionIds: string[], message: string): void { this.notificationService.prompt(Severity.Info, message, [{ - label: localize('install', 'Install'), - run: () => { - this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId }); - this.runAction(this.instantiationService.createInstance(InstallRecommendedExtensionAction, extensionId)); + label: extensionIds.length === 1 ? localize('install', 'Install') : localize('installAll', "Install All"), + 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')); + } } }, { - label: localize('moreInformation', "More Information"), + label: extensionIds.length === 1 ? localize('moreInformation', "More Information") : localize('showRecommendations', "Show Recommendations"), run: () => { - this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId }); - this.runAction(this.instantiationService.createInstance(ShowRecommendedExtensionAction, extensionId)); + 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: choiceNever, isSecondary: true, run: () => { - this.addToImportantRecommendationsIgnore(extensionId); - this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId }); + for (const extensionId of extensionIds) { + this.addToImportantRecommendationsIgnore(extensionId); + this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId }); + } this.notificationService.prompt( Severity.Info, localize('ignoreExtensionRecommendations', "Do you want to ignore all extension recommendations?"), @@ -101,7 +115,9 @@ export abstract class ExtensionRecommendations extends Disposable { { sticky: true, onCancel: () => { - this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId }); + for (const extensionId of extensionIds) { + this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId }); + } } } ); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index 20116eb72c7..559f5346458 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -142,8 +142,8 @@ export class ExtensionRecommendationsService extends Disposable implements IExte await this.activateProactiveRecommendations(); const recommendations = [ - ...this.configBasedRecommendations.recommendations, - ...this.exeBasedRecommendations.recommendations, + ...this.configBasedRecommendations.otherRecommendations, + ...this.exeBasedRecommendations.otherRecommendations, ...this.dynamicWorkspaceRecommendations.recommendations, ...this.experimentalRecommendations.recommendations ]; @@ -159,6 +159,26 @@ export class ExtensionRecommendationsService extends Disposable implements IExte }); } + async getImportantRecommendations(): Promise { + await this.activateProactiveRecommendations(); + + const recommendations = [ + ...this.fileBasedRecommendations.importantRecommendations, + ...this.configBasedRecommendations.importantRecommendations, + ...this.exeBasedRecommendations.importantRecommendations, + ]; + + const extensionIds = distinct(recommendations.map(e => e.extensionId)) + .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); + + shuffle(extensionIds, this.sessionSeed); + + return extensionIds.map(extensionId => { + const sources: ExtensionRecommendationSource[] = distinct(recommendations.filter(r => r.extensionId === extensionId).map(r => r.source)); + return ({ extensionId, sources }); + }); + } + getKeymapRecommendations(): IExtensionRecommendation[] { return this.toExtensionRecommendations(this.keymapRecommendations.recommendations); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 06d067ad561..16ca60f34a5 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -5,12 +5,11 @@ import 'vs/css!./media/extensionActions'; import { localize } from 'vs/nls'; -import { IAction, Action } from 'vs/base/common/actions'; +import { IAction, Action, Separator, SubmenuAction } from 'vs/base/common/actions'; import { Delayer } from 'vs/base/common/async'; import * as DOM from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import * as json from 'vs/base/common/json'; -import { ActionViewItem, Separator, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { dispose, Disposable } from 'vs/base/common/lifecycle'; import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, AutoUpdateConfigurationKey, IExtensionContainer, EXTENSIONS_CONFIG, TOGGLE_IGNORE_EXTENSION_ACTION_ID } from 'vs/workbench/contrib/extensions/common/extensions'; @@ -61,6 +60,7 @@ import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/d import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { Codicon } from 'vs/base/common/codicons'; import { IViewsService } from 'vs/workbench/common/views'; +import { IActionViewItemOptions, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; export function toExtensionDescription(local: ILocalExtension): IExtensionDescription { return { @@ -710,7 +710,7 @@ export class DropDownMenuActionViewItem extends ExtensionActionViewItem { } } -export function getContextMenuActions(menuService: IMenuService, contextKeyService: IContextKeyService, instantiationService: IInstantiationService, extension: IExtension | undefined | null): ExtensionAction[][] { +export function getContextMenuActions(menuService: IMenuService, contextKeyService: IContextKeyService, instantiationService: IInstantiationService, extension: IExtension | undefined | null): IAction[][] { const scopedContextKeyService = contextKeyService.createScoped(); if (extension) { scopedContextKeyService.createKey('extension', extension.identifier.id); @@ -721,9 +721,14 @@ export function getContextMenuActions(menuService: IMenuService, contextKeyServi } } - const groups: ExtensionAction[][] = []; + const groups: IAction[][] = []; const menu = menuService.createMenu(MenuId.ExtensionContext, scopedContextKeyService); - menu.getActions({ shouldForwardArgs: true }).forEach(([, actions]) => groups.push(actions.map(action => instantiationService.createInstance(MenuItemExtensionAction, action)))); + menu.getActions({ shouldForwardArgs: true }).forEach(([, actions]) => groups.push(actions.map(action => { + if (action instanceof SubmenuAction) { + return action; + } + return instantiationService.createInstance(MenuItemExtensionAction, action); + }))); menu.dispose(); return groups; @@ -752,7 +757,7 @@ export class ManageExtensionAction extends ExtensionDropDownAction { } async getActionGroups(runningExtensions: IExtensionDescription[]): Promise { - const groups: ExtensionAction[][] = []; + const groups: IAction[][] = []; if (this.extension) { const actions = await Promise.all([ SetColorThemeAction.create(this.workbenchThemeService, this.instantiationService, this.extension), @@ -783,7 +788,11 @@ export class ManageExtensionAction extends ExtensionDropDownAction { getContextMenuActions(this.menuService, this.contextKeyService, this.instantiationService, this.extension).forEach(actions => groups.push(actions)); - groups.forEach(group => group.forEach(extensionAction => extensionAction.extension = this.extension)); + groups.forEach(group => group.forEach(extensionAction => { + if (extensionAction instanceof ExtensionAction) { + extensionAction.extension = this.extension; + } + })); return groups; } @@ -1083,7 +1092,6 @@ export class CheckForUpdatesAction extends Action { @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IViewletService private readonly viewletService: IViewletService, - @IDialogService private readonly dialogService: IDialogService, @INotificationService private readonly notificationService: INotificationService ) { super(id, label, '', true); @@ -1092,7 +1100,7 @@ export class CheckForUpdatesAction extends Action { private checkUpdatesAndNotify(): void { const outdated = this.extensionsWorkbenchService.outdated; if (!outdated.length) { - this.dialogService.show(Severity.Info, localize('noUpdatesAvailable', "All extensions are up to date."), [localize('ok', "OK")]); + this.notificationService.info(localize('noUpdatesAvailable', "All extensions are up to date.")); return; } @@ -1748,7 +1756,51 @@ export class ShowPopularExtensionsAction extends Action { return this.viewletService.openViewlet(VIEWLET_ID, true) .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) .then(viewlet => { - viewlet.search('@sort:installs '); + viewlet.search('@popular '); + viewlet.focus(); + }); + } +} + +export class PredefinedExtensionFilterAction extends Action { + + constructor( + id: string, + label: string, + private readonly filter: 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(`${this.filter} `); + viewlet.focus(); + }); + } +} + +export class RecentlyPublishedExtensionsAction extends Action { + + static readonly ID = 'workbench.extensions.action.recentlyPublishedExtensions'; + static readonly LABEL = localize('recentlyPublishedExtensions', "Recently Published 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:publishedDate '); viewlet.focus(); }); } @@ -1777,19 +1829,20 @@ export class ShowRecommendedExtensionsAction extends Action { } } -export class InstallWorkspaceRecommendedExtensionsAction extends Action { +export class InstallRecommendedExtensionsAction extends Action { - static readonly ID = 'workbench.extensions.action.installWorkspaceRecommendedExtensions'; - static readonly LABEL = localize('installWorkspaceRecommendedExtensions', "Install All Workspace Recommended Extensions"); + 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 = InstallWorkspaceRecommendedExtensionsAction.ID, - label: string = InstallWorkspaceRecommendedExtensionsAction.LABEL, + 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, @@ -1808,7 +1861,7 @@ export class InstallWorkspaceRecommendedExtensionsAction extends Action { viewlet.search('@recommended '); viewlet.focus(); const names = this.recommendations; - return this.extensionWorkbenchService.queryGallery({ names, source: 'install-all-workspace-recommendations' }, CancellationToken.None).then(pager => { + 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++) { @@ -1840,6 +1893,22 @@ export class InstallWorkspaceRecommendedExtensionsAction extends Action { } } +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'; @@ -2029,6 +2098,27 @@ export class ShowAzureExtensionsAction extends Action { } } +export class SearchCategoryAction extends Action { + + constructor( + id: string, + label: string, + private readonly category: 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(`@category:"${this.category.toLowerCase()}"`); + viewlet.focus(); + }); + } +} + export class ChangeSortAction extends Action { private query: Query; @@ -2712,7 +2802,7 @@ export class SyncIgnoredIconAction extends ExtensionAction { @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, ) { super('extensions.syncignore', '', SyncIgnoredIconAction.DISABLE_CLASS, false); - this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectedKeys.includes('sync.ignoredExtensions'))(() => this.update())); + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectedKeys.includes('settingsSync.ignoredExtensions'))(() => this.update())); this.update(); this.tooltip = localize('syncingore.label', "This extension is ignored during sync."); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index 4c8e6373219..975786bc6f6 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -138,6 +138,7 @@ export class Renderer implements IPagedRenderer { addClass(data.element, 'loading'); data.root.removeAttribute('aria-label'); + data.root.removeAttribute('data-extension-id'); data.extensionDisposables = dispose(data.extensionDisposables); data.icon.src = ''; data.name.textContent = ''; @@ -150,6 +151,7 @@ export class Renderer implements IPagedRenderer { renderElement(extension: IExtension, index: number, data: ITemplateData): void { removeClass(data.element, 'loading'); + data.root.setAttribute('data-extension-id', extension.identifier.id); if (extension.state !== ExtensionState.Uninstalled && !extension.server) { // Get the extension if it is installed and has no server information diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts index 89ac951350f..5ee60dcc409 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts @@ -271,7 +271,8 @@ export class ExtensionsTree extends WorkbenchAsyncDataTree>{ getAriaLabel(extensionData: IExtensionData): string { - return localize('extension-arialabel', "{0}. Press enter for extension details.", extensionData.extension.displayName); + const extension = extensionData.extension; + return localize('extension-arialabel', "{0}, {1}, {2}, press enter for extension details.", extension.displayName, extension.version, extension.publisherDisplayName); }, getWidgetAriaLabel(): string { return localize('extensions', "Extensions"); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index ccd37e31f32..a154ca87f76 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -10,24 +10,24 @@ import { isPromiseCanceledError } from 'vs/base/common/errors'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { Event as EventOf, Emitter } from 'vs/base/common/event'; -import { IAction, Action } from 'vs/base/common/actions'; -import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IAction, Action, Separator, SubmenuAction } from 'vs/base/common/actions'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { append, $, addClass, toggleClass, Dimension, hide, show } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, AutoUpdateConfigurationKey, ShowRecommendationsOnlyOnDemandKey, CloseExtensionDetailsOnViewChangeKey } from '../common/extensions'; +import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, AutoUpdateConfigurationKey, CloseExtensionDetailsOnViewChangeKey } from '../common/extensions'; import { - ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowRecommendedExtensionsAction, ShowPopularExtensionsAction, ShowDisabledExtensionsAction, - ShowOutdatedExtensionsAction, ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction, CheckForUpdatesAction, DisableAllAction, EnableAllAction, - EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction + ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction, CheckForUpdatesAction, DisableAllAction, EnableAllAction, + EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction, SearchCategoryAction, + RecentlyPublishedExtensionsAction, ShowInstalledExtensionsAction, ShowOutdatedExtensionsAction, ShowDisabledExtensionsAction, + ShowEnabledExtensionsAction, PredefinedExtensionFilterAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; -import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; -import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInExtensionsView, BuiltInThemesExtensionsView, BuiltInBasicsExtensionsView, ServerExtensionsView, DefaultRecommendedExtensionsView } from 'vs/workbench/contrib/extensions/browser/extensionsViews'; +import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInFeatureExtensionsView, BuiltInThemesExtensionsView, BuiltInProgrammingLanguageExtensionsView, ServerExtensionsView, DefaultRecommendedExtensionsView, OutdatedExtensionsView, InstalledExtensionsView, SearchBuiltInExtensionsView } from 'vs/workbench/contrib/extensions/browser/extensionsViews'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import Severity from 'vs/base/common/severity'; @@ -50,9 +50,8 @@ import { SuggestEnabledInput, attachSuggestEnabledInputBoxStyler } from 'vs/work import { alert } from 'vs/base/browser/ui/aria/aria'; import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { RemoteNameContext } from 'vs/workbench/browser/contextkeys'; import { ILabelService } from 'vs/platform/label/common/label'; import { MementoObject } from 'vs/workbench/common/memento'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -69,24 +68,9 @@ const SearchOutdatedExtensionsContext = new RawContextKey('searchOutdat const SearchEnabledExtensionsContext = new RawContextKey('searchEnabledExtensions', false); const SearchDisabledExtensionsContext = new RawContextKey('searchDisabledExtensions', false); const HasInstalledExtensionsContext = new RawContextKey('hasInstalledExtensions', true); +const BuiltInExtensionsContext = new RawContextKey('builtInExtensions', false); const SearchBuiltInExtensionsContext = new RawContextKey('searchBuiltInExtensions', false); const RecommendedExtensionsContext = new RawContextKey('recommendedExtensions', false); -const DefaultRecommendedExtensionsContext = new RawContextKey('defaultRecommendedExtensions', false); -const viewIdNameMappings: { [id: string]: string } = { - 'extensions.listView': localize('marketPlace', "Marketplace"), - 'extensions.enabledExtensionList': localize('enabledExtensions', "Enabled"), - 'extensions.enabledExtensionList2': localize('enabledExtensions', "Enabled"), - 'extensions.disabledExtensionList': localize('disabledExtensions', "Disabled"), - 'extensions.disabledExtensionList2': localize('disabledExtensions', "Disabled"), - 'extensions.popularExtensionsList': localize('popularExtensions', "Popular"), - 'extensions.recommendedList': localize('recommendedExtensions', "Recommended"), - 'extensions.otherrecommendedList': localize('otherRecommendedExtensions', "Other Recommendations"), - 'extensions.workspaceRecommendedList': localize('workspaceRecommendedExtensions', "Workspace Recommendations"), - 'extensions.builtInExtensionsList': localize('builtInExtensions', "Features"), - 'extensions.builtInThemesExtensionsList': localize('builtInThemesExtensions', "Themes"), - 'extensions.builtInBasicsExtensionsList': localize('builtInBasicsExtensions', "Programming Languages"), - 'extensions.syncedExtensionsList': localize('syncedExtensions', "My Account"), -}; export class ExtensionsViewletViewsContribution implements IWorkbenchContribution { @@ -102,220 +86,236 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio } private registerViews(): void { - let viewDescriptors: IViewDescriptor[] = []; - viewDescriptors.push(this.createMarketPlaceExtensionsListViewDescriptor()); - viewDescriptors.push(this.createDefaultEnabledExtensionsListViewDescriptor()); - viewDescriptors.push(this.createDefaultDisabledExtensionsListViewDescriptor()); - viewDescriptors.push(this.createDefaultPopularExtensionsListViewDescriptor()); - viewDescriptors.push(this.createEnabledExtensionsListViewDescriptor()); - viewDescriptors.push(this.createDisabledExtensionsListViewDescriptor()); - viewDescriptors.push(this.createBuiltInExtensionsListViewDescriptor()); - viewDescriptors.push(this.createBuiltInBasicsExtensionsListViewDescriptor()); - viewDescriptors.push(this.createBuiltInThemesExtensionsListViewDescriptor()); - viewDescriptors.push(this.createDefaultRecommendedExtensionsListViewDescriptor()); - viewDescriptors.push(this.createOtherRecommendedExtensionsListViewDescriptor()); - viewDescriptors.push(this.createWorkspaceRecommendedExtensionsListViewDescriptor()); + const viewDescriptors: IViewDescriptor[] = []; - if (this.extensionManagementServerService.localExtensionManagementServer) { - viewDescriptors.push(...this.createExtensionsViewDescriptorsForServer(this.extensionManagementServerService.localExtensionManagementServer)); - } - if (this.extensionManagementServerService.remoteExtensionManagementServer) { - viewDescriptors.push(...this.createExtensionsViewDescriptorsForServer(this.extensionManagementServerService.remoteExtensionManagementServer)); - } + /* Default views */ + viewDescriptors.push(...this.createDefaultExtensionsViewDescriptors()); + + /* Search views */ + viewDescriptors.push(...this.createSearchExtensionsViewDescriptors()); + + /* Recommendations views */ + viewDescriptors.push(...this.createRecommendedExtensionsViewDescriptors()); + + /* Built-in extensions views */ + viewDescriptors.push(...this.createBuiltinExtensionsViewDescriptors()); Registry.as(Extensions.ViewsRegistry).registerViews(viewDescriptors, this.container); } - // View used for any kind of searching - private createMarketPlaceExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.listView'; - return { - id, - name: viewIdNameMappings[id], - ctorDescriptor: new SyncDescriptor(ExtensionsListView), - when: ContextKeyExpr.and(ContextKeyExpr.has('searchMarketplaceExtensions')), - weight: 100 - }; - } + private createDefaultExtensionsViewDescriptors(): IViewDescriptor[] { + const viewDescriptors: IViewDescriptor[] = []; - // Separate view for enabled extensions required as we need to show enabled, disabled and recommended sections - // in the default view when there is no search text, but user has installed extensions. - private createDefaultEnabledExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.enabledExtensionList'; - return { - id, - name: viewIdNameMappings[id], - ctorDescriptor: new SyncDescriptor(EnabledExtensionsView), - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), RemoteNameContext.isEqualTo('')), - weight: 40, - canToggleVisibility: true, - order: 1 - }; - } - - // Separate view for disabled extensions required as we need to show enabled, disabled and recommended sections - // in the default view when there is no search text, but user has installed extensions. - private createDefaultDisabledExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.disabledExtensionList'; - return { - id, - name: viewIdNameMappings[id], - ctorDescriptor: new SyncDescriptor(DisabledExtensionsView), - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), RemoteNameContext.isEqualTo('')), - weight: 10, - canToggleVisibility: true, - order: 3, - collapsed: true - }; - } - - // Separate view for popular extensions required as we need to show popular and recommended sections - // in the default view when there is no search text, and user has no installed extensions. - private createDefaultPopularExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.popularExtensionsList'; - return { - id, - name: viewIdNameMappings[id], + /* + * Default popular extensions view + * Separate view for popular extensions required as we need to show popular and recommended sections + * in the default view when there is no search text, and user has no installed extensions. + */ + viewDescriptors.push({ + id: 'workbench.views.extensions.popular', + name: localize('popularExtensions', "Popular"), ctorDescriptor: new SyncDescriptor(ExtensionsListView), when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.not('hasInstalledExtensions')), weight: 60, - order: 1 - }; - } + order: 1, + }); - private createExtensionsViewDescriptorsForServer(server: IExtensionManagementServer): IViewDescriptor[] { + /* + * Default installed extensions views - Shows all user installed extensions. + */ + const servers: IExtensionManagementServer[] = []; + if (this.extensionManagementServerService.localExtensionManagementServer) { + servers.push(this.extensionManagementServerService.localExtensionManagementServer); + } + 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 => { - const serverLabel = server.label; - if (viewTitle && this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { - return `${serverLabel} - ${viewTitle}`; - } - return viewTitle ? viewTitle : serverLabel; + return servers.length > 1 ? `${server.label} - ${viewTitle}` : viewTitle; }; - const getInstalledViewName = (): string => getViewName(localize('installed', "Installed"), server); - const getOutdatedViewName = (): string => getViewName(localize('outdated', "Outdated"), server); - const onDidChangeServerLabel: EventOf = EventOf.map(this.labelService.onDidChangeFormatters, () => undefined); - return [{ - id: `extensions.${server.id}.installed`, - get name() { return getInstalledViewName(); }, - ctorDescriptor: new SyncDescriptor(ServerExtensionsView, [server, EventOf.map(onDidChangeServerLabel, () => getInstalledViewName())]), - when: ContextKeyExpr.and(ContextKeyExpr.has('searchInstalledExtensions')), - weight: 100 - }, { - id: `extensions.${server.id}.outdated`, - get name() { return getOutdatedViewName(); }, - ctorDescriptor: new SyncDescriptor(ServerExtensionsView, [server, EventOf.map(onDidChangeServerLabel, () => getOutdatedViewName())]), - when: ContextKeyExpr.and(ContextKeyExpr.has('searchOutdatedExtensions')), - weight: 100 - }, { - id: `extensions.${server.id}.default`, - get name() { return getInstalledViewName(); }, - ctorDescriptor: new SyncDescriptor(ServerExtensionsView, [server, EventOf.map(onDidChangeServerLabel, () => getInstalledViewName())]), - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), RemoteNameContext.notEqualsTo('')), - weight: 40, - order: 1 - }]; - } + for (const server of servers) { + const getInstalledViewName = (): string => getViewName(localize('installed', "Installed"), server); + const onDidChangeServerLabel: EventOf = EventOf.map(this.labelService.onDidChangeFormatters, () => undefined); + viewDescriptors.push({ + id: servers.length > 1 ? `workbench.views.extensions.${server.id}.installed` : `workbench.views.extensions.installed`, + get name() { return getInstalledViewName(); }, + ctorDescriptor: new SyncDescriptor(ServerExtensionsView, [server, EventOf.map(onDidChangeServerLabel, () => getInstalledViewName())]), + when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions')), + weight: 100, + order: 2, + /* Installed extensions views shall not be hidden when there are more than one server */ + canToggleVisibility: servers.length === 1 + }); + } - // Separate view for recommended extensions required as we need to show it along with other views when there is no search text. - // When user has installed extensions, this is shown along with the views for enabled & disabled extensions - // When user has no installed extensions, this is shown along with the view for popular extensions - private createDefaultRecommendedExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.recommendedList'; - return { - id, - name: viewIdNameMappings[id], + /* + * Default recommended extensions view + * When user has installed extensions, this is shown along with the views for enabled & disabled extensions + * When user has no installed extensions, this is shown along with the view for popular extensions + */ + viewDescriptors.push({ + id: 'extensions.recommendedList', + name: localize('recommendedExtensions', "Recommended"), ctorDescriptor: new SyncDescriptor(DefaultRecommendedExtensionsView), - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('defaultRecommendedExtensions')), + when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.not('config.extensions.showRecommendationsOnlyOnDemand')), weight: 40, - order: 2, + order: 3, canToggleVisibility: true - }; + }); + + /* Installed views shall be default in multi server window */ + if (servers.length === 1) { + /* + * Default enabled extensions view - Shows all user installed enabled extensions. + * Hidden by default + */ + viewDescriptors.push({ + id: 'workbench.views.extensions.enabled', + name: localize('enabledExtensions', "Enabled"), + ctorDescriptor: new SyncDescriptor(EnabledExtensionsView), + when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions')), + hideByDefault: true, + weight: 40, + order: 4, + canToggleVisibility: true + }); + + /* + * Default disabled extensions view - Shows all disabled extensions. + * Hidden by default + */ + viewDescriptors.push({ + id: 'workbench.views.extensions.disabled', + name: localize('disabledExtensions', "Disabled"), + ctorDescriptor: new SyncDescriptor(DisabledExtensionsView), + when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions')), + hideByDefault: true, + weight: 10, + order: 5, + canToggleVisibility: true + }); + + } + + return viewDescriptors; } - // Separate view for recommedations that are not workspace recommendations. - // Shown along with view for workspace recommendations, when using the command that shows recommendations - private createOtherRecommendedExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.otherrecommendedList'; - return { - id, - name: viewIdNameMappings[id], - ctorDescriptor: new SyncDescriptor(RecommendedExtensionsView), - when: ContextKeyExpr.has('recommendedExtensions'), - weight: 50, - order: 2 - }; - } + private createSearchExtensionsViewDescriptors(): IViewDescriptor[] { + const viewDescriptors: IViewDescriptor[] = []; - // Separate view for workspace recommendations. - // Shown along with view for other recommendations, when using the command that shows recommendations - private createWorkspaceRecommendedExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.workspaceRecommendedList'; - return { - id, - name: viewIdNameMappings[id], - ctorDescriptor: new SyncDescriptor(WorkspaceRecommendedExtensionsView), - when: ContextKeyExpr.and(ContextKeyExpr.has('recommendedExtensions'), ContextKeyExpr.has('nonEmptyWorkspace')), - weight: 50, - order: 1 - }; - } + /* + * View used for searching Marketplace + */ + viewDescriptors.push({ + id: 'workbench.views.extensions.marketplace', + name: localize('marketPlace', "Marketplace"), + ctorDescriptor: new SyncDescriptor(ExtensionsListView), + when: ContextKeyExpr.and(ContextKeyExpr.has('searchMarketplaceExtensions')), + }); - private createEnabledExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.enabledExtensionList2'; - return { - id, - name: viewIdNameMappings[id], + /* + * View used for searching all installed extensions + */ + viewDescriptors.push({ + id: 'workbench.views.extensions.searchInstalled', + name: localize('installed', "Installed"), + ctorDescriptor: new SyncDescriptor(InstalledExtensionsView), + when: ContextKeyExpr.and(ContextKeyExpr.has('searchInstalledExtensions')), + }); + + /* + * View used for searching enabled extensions + */ + viewDescriptors.push({ + id: 'workbench.views.extensions.searchEnabled', + name: localize('enabled', "Enabled"), ctorDescriptor: new SyncDescriptor(EnabledExtensionsView), when: ContextKeyExpr.and(ContextKeyExpr.has('searchEnabledExtensions')), - weight: 40, - order: 1 - }; - } + }); - private createDisabledExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.disabledExtensionList2'; - return { - id, - name: viewIdNameMappings[id], + /* + * View used for searching disabled extensions + */ + viewDescriptors.push({ + id: 'workbench.views.extensions.searchDisabled', + name: localize('disabled', "Disabled"), ctorDescriptor: new SyncDescriptor(DisabledExtensionsView), when: ContextKeyExpr.and(ContextKeyExpr.has('searchDisabledExtensions')), - weight: 10, - order: 3, - collapsed: true - }; + }); + + /* + * View used for searching outdated extensions + */ + viewDescriptors.push({ + id: 'workbench.views.extensions.searchOutdated', + name: localize('outdated', "Outdated"), + ctorDescriptor: new SyncDescriptor(OutdatedExtensionsView), + when: ContextKeyExpr.and(ContextKeyExpr.has('searchOutdatedExtensions')), + }); + + /* + * View used for searching builtin extensions + */ + viewDescriptors.push({ + id: 'workbench.views.extensions.searchBuiltin', + name: localize('builtin', "Builtin"), + ctorDescriptor: new SyncDescriptor(SearchBuiltInExtensionsView), + when: ContextKeyExpr.and(ContextKeyExpr.has('searchBuiltInExtensions')), + }); + + return viewDescriptors; } - private createBuiltInExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.builtInExtensionsList'; - return { - id, - name: viewIdNameMappings[id], - ctorDescriptor: new SyncDescriptor(BuiltInExtensionsView), - when: ContextKeyExpr.has('searchBuiltInExtensions'), - weight: 100 - }; + private createRecommendedExtensionsViewDescriptors(): IViewDescriptor[] { + const viewDescriptors: IViewDescriptor[] = []; + + viewDescriptors.push({ + id: 'workbench.views.extensions.workspaceRecommendations', + name: localize('workspaceRecommendedExtensions', "Workspace Recommendations"), + ctorDescriptor: new SyncDescriptor(WorkspaceRecommendedExtensionsView), + when: ContextKeyExpr.and(ContextKeyExpr.has('recommendedExtensions'), ContextKeyExpr.has('nonEmptyWorkspace')), + order: 1 + }); + + viewDescriptors.push({ + id: 'workbench.views.extensions.otherRecommendations', + name: localize('otherRecommendedExtensions', "Other Recommendations"), + ctorDescriptor: new SyncDescriptor(RecommendedExtensionsView), + when: ContextKeyExpr.has('recommendedExtensions'), + order: 2 + }); + + return viewDescriptors; } - private createBuiltInThemesExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.builtInThemesExtensionsList'; - return { - id, - name: viewIdNameMappings[id], + private createBuiltinExtensionsViewDescriptors(): IViewDescriptor[] { + const viewDescriptors: IViewDescriptor[] = []; + + viewDescriptors.push({ + id: 'workbench.views.extensions.builtinFeatureExtensions', + name: localize('builtinFeatureExtensions', "Features"), + ctorDescriptor: new SyncDescriptor(BuiltInFeatureExtensionsView), + when: ContextKeyExpr.has('builtInExtensions'), + }); + + viewDescriptors.push({ + id: 'workbench.views.extensions.builtinThemeExtensions', + name: localize('builtInThemesExtensions', "Themes"), ctorDescriptor: new SyncDescriptor(BuiltInThemesExtensionsView), - when: ContextKeyExpr.has('searchBuiltInExtensions'), - weight: 100 - }; - } + when: ContextKeyExpr.has('builtInExtensions'), + }); - private createBuiltInBasicsExtensionsListViewDescriptor(): IViewDescriptor { - const id = 'extensions.builtInBasicsExtensionsList'; - return { - id, - name: viewIdNameMappings[id], - ctorDescriptor: new SyncDescriptor(BuiltInBasicsExtensionsView), - when: ContextKeyExpr.has('searchBuiltInExtensions'), - weight: 100 - }; + viewDescriptors.push({ + id: 'workbench.views.extensions.builtinProgrammingLanguageExtensions', + name: localize('builtinProgrammingLanguageExtensions', "Programming Languages"), + ctorDescriptor: new SyncDescriptor(BuiltInProgrammingLanguageExtensionsView), + when: ContextKeyExpr.has('builtInExtensions'), + }); + + return viewDescriptors; } } @@ -332,16 +332,15 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE private searchEnabledExtensionsContextKey: IContextKey; private searchDisabledExtensionsContextKey: IContextKey; private hasInstalledExtensionsContextKey: IContextKey; + private builtInExtensionsContextKey: IContextKey; private searchBuiltInExtensionsContextKey: IContextKey; private recommendedExtensionsContextKey: IContextKey; - private defaultRecommendedExtensionsContextKey: IContextKey; private searchDelayer: Delayer; private root: HTMLElement | undefined; private searchBox: SuggestEnabledInput | undefined; - private primaryActions: IAction[] | undefined; - private secondaryActions: IAction[] | null = null; private readonly searchViewletState: MementoObject; + private readonly sortActions: ChangeSortAction[]; constructor( @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @@ -350,6 +349,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE @IInstantiationService instantiationService: IInstantiationService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @INotificationService private readonly notificationService: INotificationService, @IViewletService private readonly viewletService: IViewletService, @IThemeService themeService: IThemeService, @@ -360,7 +360,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE @IContextMenuService contextMenuService: IContextMenuService, @IExtensionService extensionService: IExtensionService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IPreferencesService private readonly preferencesService: IPreferencesService + @IPreferencesService private readonly preferencesService: IPreferencesService, ) { super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); @@ -373,10 +373,9 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.searchEnabledExtensionsContextKey = SearchEnabledExtensionsContext.bindTo(contextKeyService); this.searchDisabledExtensionsContextKey = SearchDisabledExtensionsContext.bindTo(contextKeyService); this.hasInstalledExtensionsContextKey = HasInstalledExtensionsContext.bindTo(contextKeyService); + this.builtInExtensionsContextKey = BuiltInExtensionsContext.bindTo(contextKeyService); this.searchBuiltInExtensionsContextKey = SearchBuiltInExtensionsContext.bindTo(contextKeyService); this.recommendedExtensionsContextKey = RecommendedExtensionsContext.bindTo(contextKeyService); - this.defaultRecommendedExtensionsContextKey = DefaultRecommendedExtensionsContext.bindTo(contextKeyService); - this.defaultRecommendedExtensionsContextKey.set(!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)); this._register(this.viewletService.onDidViewletOpen(this.onViewletOpen, this)); this.searchViewletState = this.getMemento(StorageScope.WORKSPACE); @@ -386,13 +385,16 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AutoUpdateConfigurationKey)) { - this.secondaryActions = null; this.updateTitleArea(); } - if (e.affectedKeys.indexOf(ShowRecommendationsOnlyOnDemandKey) > -1) { - this.defaultRecommendedExtensionsContextKey.set(!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)); - } }, this)); + + this.sortActions = [ + this._register(this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.install', localize('sort by installs', "Install Count"), this.onSearchChange, 'installs')), + this._register(this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.rating', localize('sort by rating', "Rating"), this.onSearchChange, 'rating')), + this._register(this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.name', localize('sort by name', "Name"), this.onSearchChange, 'name')), + this._register(this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.publishedDate', localize('sort by date', "Published Date"), this.onSearchChange, 'publishedDate')), + ]; } create(parent: HTMLElement): void { @@ -505,39 +507,56 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE } getActions(): IAction[] { - if (!this.primaryActions) { - this.primaryActions = [ - this.instantiationService.createInstance(ClearExtensionsInputAction, ClearExtensionsInputAction.ID, ClearExtensionsInputAction.LABEL, this.onSearchChange, this.searchBox ? this.searchBox.getValue() : '') - ]; + const filterActions: IAction[] = []; + + // Local extensions filters + filterActions.push(...[ + this.instantiationService.createInstance(ShowBuiltInExtensionsAction, ShowBuiltInExtensionsAction.ID, localize('builtin filter', "Built-in")), + this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, localize('installed filter', "Installed")), + this.instantiationService.createInstance(ShowEnabledExtensionsAction, ShowEnabledExtensionsAction.ID, localize('enabled filter', "Enabled")), + this.instantiationService.createInstance(ShowDisabledExtensionsAction, ShowDisabledExtensionsAction.ID, localize('disabled filter', "Disabled")), + this.instantiationService.createInstance(ShowOutdatedExtensionsAction, ShowOutdatedExtensionsAction.ID, localize('outdated filter', "Outdated")), + ]); + + if (this.extensionGalleryService.isEnabled()) { + filterActions.splice(0, 0, ...[ + 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 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(), + ]); + filterActions.push(...[ + new Separator(), + new SubmenuAction('workbench.extensions.action.sortBy', localize('sorty by', "Sort By"), this.sortActions), + ]); } - return this.primaryActions; + + return [ + new SubmenuAction('workbench.extensions.action.filterExtensions', localize('filterExtensions', "Filter Extensions..."), filterActions, 'codicon-filter'), + this.instantiationService.createInstance(ClearExtensionsInputAction, ClearExtensionsInputAction.ID, ClearExtensionsInputAction.LABEL, this.onSearchChange, this.searchBox ? this.searchBox.getValue() : ''), + ]; } getSecondaryActions(): IAction[] { - if (!this.secondaryActions) { - this.secondaryActions = [ - this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL), - this.instantiationService.createInstance(ShowOutdatedExtensionsAction, ShowOutdatedExtensionsAction.ID, ShowOutdatedExtensionsAction.LABEL), - this.instantiationService.createInstance(ShowEnabledExtensionsAction, ShowEnabledExtensionsAction.ID, ShowEnabledExtensionsAction.LABEL), - this.instantiationService.createInstance(ShowDisabledExtensionsAction, ShowDisabledExtensionsAction.ID, ShowDisabledExtensionsAction.LABEL), - this.instantiationService.createInstance(ShowBuiltInExtensionsAction, ShowBuiltInExtensionsAction.ID, ShowBuiltInExtensionsAction.LABEL), - this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, ShowRecommendedExtensionsAction.LABEL), - this.instantiationService.createInstance(ShowPopularExtensionsAction, ShowPopularExtensionsAction.ID, ShowPopularExtensionsAction.LABEL), - new Separator(), - this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.install', localize('sort by installs', "Sort By: Install Count"), this.onSearchChange, 'installs'), - this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.rating', localize('sort by rating', "Sort By: Rating"), this.onSearchChange, 'rating'), - this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.name', localize('sort by name', "Sort By: Name"), this.onSearchChange, 'name'), - new Separator(), - this.instantiationService.createInstance(CheckForUpdatesAction, CheckForUpdatesAction.ID, CheckForUpdatesAction.LABEL), - ...(this.configurationService.getValue(AutoUpdateConfigurationKey) ? [this.instantiationService.createInstance(DisableAutoUpdateAction, DisableAutoUpdateAction.ID, DisableAutoUpdateAction.LABEL)] : [this.instantiationService.createInstance(UpdateAllAction, UpdateAllAction.ID, UpdateAllAction.LABEL), this.instantiationService.createInstance(EnableAutoUpdateAction, EnableAutoUpdateAction.ID, EnableAutoUpdateAction.LABEL)]), - this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL), - new Separator(), - this.instantiationService.createInstance(DisableAllAction, DisableAllAction.ID, DisableAllAction.LABEL), - this.instantiationService.createInstance(EnableAllAction, EnableAllAction.ID, EnableAllAction.LABEL) - ]; + const actions: IAction[] = []; + + actions.push(this.instantiationService.createInstance(CheckForUpdatesAction, CheckForUpdatesAction.ID, CheckForUpdatesAction.LABEL)); + if (this.configurationService.getValue(AutoUpdateConfigurationKey)) { + actions.push(this.instantiationService.createInstance(DisableAutoUpdateAction, DisableAutoUpdateAction.ID, DisableAutoUpdateAction.LABEL)); + } else { + actions.push(this.instantiationService.createInstance(UpdateAllAction, UpdateAllAction.ID, UpdateAllAction.LABEL), this.instantiationService.createInstance(EnableAutoUpdateAction, EnableAutoUpdateAction.ID, EnableAutoUpdateAction.LABEL)); } - return this.secondaryActions; + actions.push(new Separator()); + actions.push(this.instantiationService.createInstance(EnableAllAction, EnableAllAction.ID, EnableAllAction.LABEL)); + actions.push(this.instantiationService.createInstance(DisableAllAction, DisableAllAction.ID, DisableAllAction.LABEL)); + + actions.push(new Separator()); + actions.push(this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL)); + + return actions; } search(value: string, refresh: boolean = false): void { @@ -555,7 +574,14 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE } private normalizedQuery(): string { - return this.searchBox ? this.searchBox.getValue().replace(/@category/g, 'category').replace(/@tag:/g, 'tag:').replace(/@ext:/g, 'ext:') : ''; + return this.searchBox + ? this.searchBox.getValue() + .replace(/@category/g, 'category') + .replace(/@tag:/g, 'tag:') + .replace(/@ext:/g, 'ext:') + .replace(/@featured/g, 'featured') + .replace(/@popular/g, '@sort:installs') + : ''; } saveState(): void { @@ -575,7 +601,8 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.searchOutdatedExtensionsContextKey.set(ExtensionsListView.isOutdatedExtensionsQuery(value)); this.searchEnabledExtensionsContextKey.set(ExtensionsListView.isEnabledExtensionsQuery(value)); this.searchDisabledExtensionsContextKey.set(ExtensionsListView.isDisabledExtensionsQuery(value)); - this.searchBuiltInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value)); + this.searchBuiltInExtensionsContextKey.set(ExtensionsListView.isSearchBuiltInExtensionsQuery(value)); + this.builtInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value)); this.recommendedExtensionsContextKey.set(isRecommendedExtensionsQuery); this.searchMarketplaceExtensionsContextKey.set(!!value && !ExtensionsListView.isLocalExtensionsQuery(value) && !isRecommendedExtensionsQuery); this.nonEmptyWorkspaceContextKey.set(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY); @@ -597,19 +624,20 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE } private alertSearchResult(count: number, viewId: string): void { + const view = this.viewContainerModel.visibleViewDescriptors.find(view => view.id === viewId); switch (count) { case 0: break; case 1: - if (viewIdNameMappings[viewId]) { - alert(localize('extensionFoundInSection', "1 extension found in the {0} section.", viewIdNameMappings[viewId])); + if (view) { + alert(localize('extensionFoundInSection', "1 extension found in the {0} section.", view.name)); } else { alert(localize('extensionFound', "1 extension found.")); } break; default: - if (viewIdNameMappings[viewId]) { - alert(localize('extensionsFoundInSection', "{0} extensions found in the {1} section.", count, viewIdNameMappings[viewId])); + if (view) { + alert(localize('extensionsFoundInSection', "{0} extensions found in the {1} section.", count, view.name)); } else { alert(localize('extensionsFound', "{0} extensions found.", count)); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 3066e76a210..8e34bdaf743 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -25,8 +25,7 @@ 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 { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { InstallWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction, InstallLocalExtensionsInRemoteAction, getContextMenuActions } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { InstallWorkspaceRecommendedExtensionsAction, 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'; @@ -38,7 +37,7 @@ import { alert } from 'vs/base/browser/ui/aria/aria'; import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IAction, Action } from 'vs/base/common/actions'; +import { IAction, Action, Separator } from 'vs/base/common/actions'; import { ExtensionType, ExtensionIdentifier, IExtensionDescription, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -140,7 +139,7 @@ export class ExtensionsListView extends ViewPane { horizontalScrolling: false, accessibilityProvider: >{ getAriaLabel(extension: IExtension | null): string { - return extension ? localize('extension-arialabel', "{0}. Press enter for extension details.", extension.displayName) : ''; + return extension ? localize('extension-arialabel', "{0}, {1}, {2}, press enter for extension details.", extension.displayName, extension.version, extension.publisherDisplayName) : ''; }, getWidgetAriaLabel(): string { return localize('extensions', "Extensions"); @@ -197,6 +196,7 @@ export class ExtensionsListView extends ViewPane { case 'installs': options = assign(options, { sortBy: SortBy.InstallCount }); break; case 'rating': options = assign(options, { sortBy: SortBy.WeightedRating }); break; case 'name': options = assign(options, { sortBy: SortBy.Title }); break; + case 'publishedDate': options = assign(options, { sortBy: SortBy.PublishedDate }); break; } const successCallback = (model: IPagedModel) => { @@ -247,7 +247,11 @@ export class ExtensionsListView extends ViewPane { }); } else if (e.element) { const groups = getContextMenuActions(this.menuService, this.contextKeyService.createScoped(), this.instantiationService, e.element); - groups.forEach(group => group.forEach(extensionAction => extensionAction.extension = e.element!)); + groups.forEach(group => group.forEach(extensionAction => { + if (extensionAction instanceof ExtensionAction) { + extensionAction.extension = e.element!; + } + })); let actions: IAction[] = []; for (const menuActions of groups) { actions = [...actions, ...menuActions, new Separator()]; @@ -547,10 +551,11 @@ export class ExtensionsListView extends ViewPane { 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]) - .then(([others, workspaceRecommendations, configBasedRecommendations]) => { - const names = this.getTrimmedRecommendations(local, value, fileBasedRecommendations, configBasedRecommendations, others, workspaceRecommendations); + 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" : { @@ -603,14 +608,15 @@ export class ExtensionsListView extends ViewPane { 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]) - .then(([others, workspaceRecommendations, configBasedRecommendations]) => { + 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 names = this.getTrimmedRecommendations(local, value, fileBasedRecommendations, configBasedRecommendations, others, []); + const names = this.getTrimmedRecommendations(local, value, importantRecommendations, fileBasedRecommendations, configBasedRecommendations, others, []); const recommendationsWithReason = this.tipsService.getAllRecommendationsWithReason(); /* __GDPR__ @@ -643,22 +649,30 @@ export class ExtensionsListView extends ViewPane { } // Given all recommendations, trims and returns recommendations in the relevant order after filtering out installed extensions - private getTrimmedRecommendations(installedExtensions: IExtension[], value: string, fileBasedRecommendations: IExtensionRecommendation[], configBasedRecommendations: IExtensionRecommendation[], otherRecommendations: IExtensionRecommendation[], workspaceRecommendations: IExtensionRecommendation[]): string[] { + 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; }); - configBasedRecommendations = configBasedRecommendations + 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; }); @@ -666,13 +680,14 @@ export class ExtensionsListView extends ViewPane { 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; }); const otherCount = Math.min(2, otherRecommendations.length); - const fileBasedCount = Math.min(fileBasedRecommendations.length, totalCount - workspaceRecommendations.length - configBasedRecommendations.length - otherCount); - const recommendations = [...workspaceRecommendations, ...configBasedRecommendations]; + 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)); @@ -804,16 +819,21 @@ export class ExtensionsListView extends ViewPane { this.list = null; } - static isBuiltInExtensionsQuery(query: string): boolean { - return /^\s*@builtin\s*$/i.test(query); - } - static isLocalExtensionsQuery(query: string): boolean { return this.isInstalledExtensionsQuery(query) || this.isOutdatedExtensionsQuery(query) || this.isEnabledExtensionsQuery(query) || this.isDisabledExtensionsQuery(query) - || this.isBuiltInExtensionsQuery(query); + || this.isBuiltInExtensionsQuery(query) + || this.isSearchBuiltInExtensionsQuery(query); + } + + static isSearchBuiltInExtensionsQuery(query: string): boolean { + return /@builtin\s.+/i.test(query); + } + + static isBuiltInExtensionsQuery(query: string): boolean { + return /@builtin$/i.test(query.trim()); } static isInstalledExtensionsQuery(query: string): boolean { @@ -894,7 +914,7 @@ export class ServerExtensionsView extends ExtensionsListView { async show(query: string): Promise> { query = query ? query : '@installed'; - if (!ExtensionsListView.isLocalExtensionsQuery(query) && !ExtensionsListView.isBuiltInExtensionsQuery(query)) { + if (!ExtensionsListView.isLocalExtensionsQuery(query)) { query = query += ' @installed'; } return super.show(query.trim()); @@ -926,7 +946,29 @@ export class DisabledExtensionsView extends ExtensionsListView { } } -export class BuiltInExtensionsView extends ExtensionsListView { +export class OutdatedExtensionsView extends ExtensionsListView { + + async show(query: string): Promise> { + query = query || '@outdated'; + return ExtensionsListView.isOutdatedExtensionsQuery(query) ? super.show(query) : this.showEmptyModel(); + } +} + +export class InstalledExtensionsView extends ExtensionsListView { + + async show(query: string): Promise> { + query = query || '@installed'; + return ExtensionsListView.isInstalledExtensionsQuery(query) ? super.show(query) : this.showEmptyModel(); + } +} + +export class SearchBuiltInExtensionsView extends ExtensionsListView { + async show(query: string): Promise> { + return ExtensionsListView.isSearchBuiltInExtensionsQuery(query) ? super.show(query) : this.showEmptyModel(); + } +} + +export class BuiltInFeatureExtensionsView extends ExtensionsListView { async show(query: string): Promise> { return (query && query.trim() !== '@builtin') ? this.showEmptyModel() : super.show('@builtin:features'); } @@ -938,7 +980,7 @@ export class BuiltInThemesExtensionsView extends ExtensionsListView { } } -export class BuiltInBasicsExtensionsView extends ExtensionsListView { +export class BuiltInProgrammingLanguageExtensionsView extends ExtensionsListView { async show(query: string): Promise> { return (query && query.trim() !== '@builtin') ? this.showEmptyModel() : super.show('@builtin:basics'); } @@ -999,7 +1041,7 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { getActions(): IAction[] { if (!this.installAllAction) { - this.installAllAction = this._register(this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction.ID, InstallWorkspaceRecommendedExtensionsAction.LABEL, [])); + this.installAllAction = this._register(this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, [])); this.installAllAction.class = 'codicon codicon-cloud-download'; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 7f11c1ca54f..996f4bb2fed 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -39,6 +39,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { asDomUri } from 'vs/base/browser/dom'; import { getIgnoredExtensions } from 'vs/platform/userDataSync/common/extensionsMerge'; import { isWeb } from 'vs/base/common/platform'; +import { getExtensionKind } from 'vs/workbench/services/extensions/common/extensionsUtil'; interface IExtensionStateProvider { (extension: Extension): T; @@ -665,14 +666,79 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (extensions.length === 1) { return extensions[0]; } + const enabledExtensions = extensions.filter(e => e.local && this.extensionEnablementService.isEnabled(e.local)); - if (enabledExtensions.length === 0) { - return extensions[0]; - } if (enabledExtensions.length === 1) { return enabledExtensions[0]; } - return enabledExtensions.find(e => e.server === this.extensionManagementServerService.remoteExtensionManagementServer) || enabledExtensions[0]; + + const extensionsToChoose = enabledExtensions.length ? enabledExtensions : extensions; + + let extension = extensionsToChoose.find(extension => { + for (const extensionKind of getExtensionKind(extension.local!.manifest, this.productService, this.configurationService)) { + switch (extensionKind) { + case 'ui': + /* UI extension is chosen only if it is installed locally */ + if (extension.server === this.extensionManagementServerService.localExtensionManagementServer) { + return true; + } + return false; + case 'workspace': + /* Choose remote workspace extension if exists */ + if (extension.server === this.extensionManagementServerService.remoteExtensionManagementServer) { + return true; + } + return false; + case 'web': + /* Choose web extension if exists */ + if (extension.server === this.extensionManagementServerService.webExtensionManagementServer) { + return true; + } + return false; + } + } + return false; + }); + + if (!extension && this.extensionManagementServerService.localExtensionManagementServer) { + extension = extensionsToChoose.find(extension => { + for (const extensionKind of getExtensionKind(extension.local!.manifest, this.productService, this.configurationService)) { + switch (extensionKind) { + case 'workspace': + /* Choose local workspace extension if exists */ + if (extension.server === this.extensionManagementServerService.localExtensionManagementServer) { + return true; + } + return false; + case 'web': + /* Choose local web extension if exists */ + if (extension.server === this.extensionManagementServerService.localExtensionManagementServer) { + return true; + } + return false; + } + } + return false; + }); + } + + if (!extension && this.extensionManagementServerService.remoteExtensionManagementServer) { + extension = extensionsToChoose.find(extension => { + for (const extensionKind of getExtensionKind(extension.local!.manifest, this.productService, this.configurationService)) { + switch (extensionKind) { + case 'web': + /* Choose remote web extension if exists */ + if (extension.server === this.extensionManagementServerService.remoteExtensionManagementServer) { + return true; + } + return false; + } + } + return false; + }); + } + + return extension || extensions[0]; } private fromGallery(gallery: IGalleryExtension, maliciousExtensionSet: Set): IExtension { @@ -911,7 +977,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension const id = extension.identifier.id.toLowerCase(); // first remove the extension completely from ignored extensions - let currentValue = [...this.configurationService.getValue('sync.ignoredExtensions')].map(id => id.toLowerCase()); + let currentValue = [...this.configurationService.getValue('settingsSync.ignoredExtensions')].map(id => id.toLowerCase()); currentValue = currentValue.filter(v => v !== id && v !== `-${id}`); // If ignored, then add only if it is ignored by default @@ -924,7 +990,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension currentValue.push(id); } - return this.configurationService.updateValue('sync.ignoredExtensions', currentValue.length ? currentValue : undefined, ConfigurationTarget.USER); + return this.configurationService.updateValue('settingsSync.ignoredExtensions', currentValue.length ? currentValue : undefined, ConfigurationTarget.USER); } private installWithProgress(installTask: () => Promise, extensionName?: string): Promise { diff --git a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts index b3f4de96701..fcc7c701ba1 100644 --- a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts @@ -3,13 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IExtensionManagementService, ILocalExtension } 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { ExtensionRecommendationSource, ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { IExtensionsViewPaneContainer, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { ExtensionRecommendationSource, ExtensionRecommendationReason, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IExtensionsViewPaneContainer, IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions'; import { CancellationToken } from 'vs/base/common/cancellation'; import { localize } from 'vs/nls'; import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage'; @@ -75,9 +74,16 @@ export class FileBasedRecommendations extends ExtensionRecommendations { return recommendations; } + get importantRecommendations(): ReadonlyArray { + return this.recommendations.filter(e => this.importantExtensionTips[e.extensionId]); + } + + get otherRecommendations(): ReadonlyArray { + return this.recommendations.filter(e => !this.importantExtensionTips[e.extensionId]); + } + constructor( isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionService private readonly extensionService: IExtensionService, @IViewletService private readonly viewletService: IViewletService, @@ -182,7 +188,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { return; } - const installed = await this.extensionManagementService.getInstalled(); + const installed = await this.extensionsWorkbenchService.queryLocal(); if (await this.promptRecommendedExtensionForFileType(recommendationsToPrompt, installed)) { return; } @@ -203,7 +209,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { this.promptRecommendedExtensionForFileExtension(fileExtension, installed); } - private async promptRecommendedExtensionForFileType(recommendations: string[], installed: ILocalExtension[]): Promise { + private async promptRecommendedExtensionForFileType(recommendations: string[], installed: IExtension[]): Promise { recommendations = this.filterIgnoredOrNotAllowed(recommendations); if (recommendations.length === 0) { @@ -226,11 +232,11 @@ export class FileBasedRecommendations extends ExtensionRecommendations { message = localize('reallyRecommendedExtensionPack', "The '{0}' extension pack is recommended for this file type.", extensionName); } - this.promptImportantExtensionInstallNotification(extensionId, message); + this.promptImportantExtensionsInstallNotification([extensionId], message); return true; } - private async promptRecommendedExtensionForFileExtension(fileExtension: string, installed: ILocalExtension[]): Promise { + private async promptRecommendedExtensionForFileExtension(fileExtension: string, installed: IExtension[]): Promise { const fileExtensionSuggestionIgnoreList = JSON.parse(this.storageService.get('extensionsAssistant/fileExtensionsSuggestionIgnore', StorageScope.GLOBAL, '[]')); if (fileExtensionSuggestionIgnoreList.indexOf(fileExtension) > -1) { return; @@ -282,8 +288,13 @@ export class FileBasedRecommendations extends ExtensionRecommendations { ); } - private filterInstalled(recommendationsToSuggest: string[], installed: ILocalExtension[]): string[] { - const installedExtensionsIds = installed.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set()); + private filterInstalled(recommendationsToSuggest: string[], installed: IExtension[]): string[] { + const installedExtensionsIds = installed.reduce((result, i) => { + if (i.enablementState !== EnablementState.DisabledByExtensionKind) { + result.add(i.identifier.id.toLowerCase()); + } + return result; + }, new Set()); return recommendationsToSuggest.filter(id => !installedExtensionsIds.has(id.toLowerCase())); } diff --git a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts index b17fd1dec71..1175c59a129 100644 --- a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts @@ -18,7 +18,7 @@ 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 { InstallWorkspaceRecommendedExtensionsAction, ShowRecommendedExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { ShowRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; @@ -120,7 +120,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { label: localize('installAll', "Install All"), run: () => { this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'install' }); - const installAllAction = this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction.ID, localize('installAll', "Install All"), recommendations.map(({ extensionId }) => extensionId)); + const installAllAction = this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, recommendations.map(({ extensionId }) => extensionId)); installAllAction.run(); installAllAction.dispose(); c(undefined); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts index 219f14298f0..1c780f7fb4f 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/runtimeExtensionsEditor'; import * as nls from 'vs/nls'; import * as os from 'os'; import { IProductService } from 'vs/platform/product/common/productService'; -import { Action, IAction } from 'vs/base/common/actions'; +import { Action, IAction, Separator } from 'vs/base/common/actions'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -18,7 +18,7 @@ import { IExtensionService, IExtensionsStatus, IExtensionHostProfile } from 'vs/ import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { append, $, addClass, toggleClass, Dimension, clearNode } from 'vs/base/browser/dom'; -import { ActionBar, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -157,23 +157,24 @@ export class RuntimeExtensionsEditor extends BaseEditor { this._extensionService.getExtensions().then((extensions) => { // We only deal with extensions with source code! this._extensionsDescriptions = extensions.filter((extension) => { - return !!extension.main; + return Boolean(extension.main) || Boolean(extension.browser); }); this._updateExtensions(); }); this._register(this._extensionService.onDidChangeExtensionsStatus(() => this._updateSoon.schedule())); } - private _updateExtensions(): void { - this._elements = this._resolveExtensions(); + private async _updateExtensions(): Promise { + this._elements = await this._resolveExtensions(); if (this._list) { this._list.splice(0, this._list.length, this._elements); } } - private _resolveExtensions(): IRuntimeExtension[] { + private async _resolveExtensions(): Promise { let marketplaceMap: { [id: string]: IExtension; } = Object.create(null); - for (let extension of this._extensionsWorkbenchService.local) { + const marketPlaceExtensions = await this._extensionsWorkbenchService.queryLocal(); + for (let extension of marketPlaceExtensions) { marketplaceMap[ExtensionIdentifier.toKey(extension.identifier.id)] = extension; } @@ -328,7 +329,7 @@ export class RuntimeExtensionsEditor extends BaseEditor { } else { data.icon.style.visibility = 'inherit'; } - data.name.textContent = element.marketplaceInfo ? element.marketplaceInfo.displayName : element.description.displayName || ''; + data.name.textContent = element.marketplaceInfo.displayName; data.version.textContent = element.description.version; const activationTimes = element.status.activationTimes!; @@ -462,11 +463,10 @@ export class RuntimeExtensionsEditor extends BaseEditor { actions.push(new ReportExtensionIssueAction(e.element, this._openerService, this._clipboardService, this._productService)); actions.push(new Separator()); - if (e.element.marketplaceInfo) { - actions.push(new Action('runtimeExtensionsEditor.action.disableWorkspace', nls.localize('disable workspace', "Disable (Workspace)"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo, EnablementState.DisabledWorkspace))); - actions.push(new Action('runtimeExtensionsEditor.action.disable', nls.localize('disable', "Disable"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo, EnablementState.DisabledGlobally))); - actions.push(new Separator()); - } + actions.push(new Action('runtimeExtensionsEditor.action.disableWorkspace', nls.localize('disable workspace', "Disable (Workspace)"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo, EnablementState.DisabledWorkspace))); + actions.push(new Action('runtimeExtensionsEditor.action.disable', nls.localize('disable', "Disable"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo, EnablementState.DisabledGlobally))); + actions.push(new Separator()); + const state = this._extensionHostProfileService.state; if (state === ProfileSessionState.Running) { actions.push(this._instantiationService.createInstance(StopExtensionHostProfileAction, StopExtensionHostProfileAction.ID, StopExtensionHostProfileAction.LABEL)); 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 58c893753df..cbbaddea537 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 @@ -14,7 +14,7 @@ import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, SortBy } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IExtensionRecommendationsService, ExtensionRecommendationReason } from 'vs/workbench/services/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'; @@ -127,6 +127,9 @@ suite('ExtensionsListView Tests', () => { { extensionId: configBasedRecommendationA.identifier.id } ]); }, + getImportantRecommendations(): Promise { + return Promise.resolve([]); + }, getFileBasedRecommendations() { return [ { extensionId: fileBasedRecommendationA.identifier.id }, diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index 82da373c436..b7f98de6930 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -14,7 +14,7 @@ import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IGalleryExtensionAssets, IExtensionIdentifier, InstallOperation, IExtensionTipsService, IGalleryMetadata } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionRecommendationsService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; @@ -34,7 +34,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { NativeURLService } from 'vs/platform/url/common/urlService'; import { URI } from 'vs/base/common/uri'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, IExtension, ExtensionKind } from 'vs/platform/extensions/common/extensions'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; @@ -46,6 +46,8 @@ import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestSer import { IExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-browser/experimentService.test'; import { ExtensionTipsService } from 'vs/platform/extensionManagement/node/extensionTipsService'; +import { Schemas } from 'vs/base/common/network'; +import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; suite('ExtensionsWorkbenchServiceTest', () => { @@ -981,6 +983,384 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.equal(actual[0].enablementState, EnablementState.DisabledWorkspace); }); + test('test user extension is preferred when the same extension exists as system and user extension', async () => { + testObject = await aWorkbenchService(); + const userExtension = aLocalExtension('pub.a'); + const systemExtension = aLocalExtension('pub.a', {}, { type: ExtensionType.System }); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [systemExtension, userExtension]); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, userExtension); + }); + + test('test user extension is disabled when the same extension exists as system and user extension and system extension is disabled', async () => { + testObject = await aWorkbenchService(); + const systemExtension = aLocalExtension('pub.a', {}, { type: ExtensionType.System }); + await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([systemExtension], EnablementState.DisabledGlobally); + const userExtension = aLocalExtension('pub.a'); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [systemExtension, userExtension]); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, userExtension); + assert.equal(actual[0].enablementState, EnablementState.DisabledGlobally); + }); + + test('Test local ui extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['ui']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local workspace extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local web extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['web']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local ui,workspace extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['ui', 'workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local workspace,ui extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['workspace', 'ui']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local ui,workspace,web extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['ui', 'workspace', 'web']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local ui,web,workspace extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['ui', 'web', 'workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local web,ui,workspace extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['web', 'ui', 'workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local web,workspace,ui extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['web', 'workspace', 'ui']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local workspace,web,ui extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['workspace', 'web', 'ui']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local workspace,ui,web extension is chosen if it exists only in local server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['workspace', 'ui', 'web']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local UI extension is chosen if it exists in both servers', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['ui']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test local ui,workspace extension is chosen if it exists in both servers', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['ui', 'workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test remote workspace extension is chosen if it exists in remote server', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['workspace']; + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService(), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, remoteExtension); + }); + + test('Test remote workspace extension is chosen if it exists in both servers', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, remoteExtension); + }); + + test('Test remote workspace extension is chosen if it exists in both servers and local is disabled', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([localExtension], EnablementState.DisabledGlobally); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, remoteExtension); + assert.equal(actual[0].enablementState, EnablementState.DisabledGlobally); + }); + + test('Test remote workspace extension is chosen if it exists in both servers and remote is disabled in workspace', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([remoteExtension], EnablementState.DisabledWorkspace); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, remoteExtension); + assert.equal(actual[0].enablementState, EnablementState.DisabledWorkspace); + }); + + test('Test local ui, workspace extension is chosen if it exists in both servers and local is disabled', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['ui', 'workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([localExtension], EnablementState.DisabledGlobally); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + assert.equal(actual[0].enablementState, EnablementState.DisabledGlobally); + }); + + test('Test local ui, workspace extension is chosen if it exists in both servers and local is disabled in workspace', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['ui', 'workspace']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([localExtension], EnablementState.DisabledWorkspace); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + assert.equal(actual[0].enablementState, EnablementState.DisabledWorkspace); + }); + + test('Test local web extension is chosen if it exists in both servers', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['web']; + const localExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`) }); + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, localExtension); + }); + + test('Test remote web extension is chosen if it exists only in remote', async () => { + // multi server setup + const extensionKind: ExtensionKind[] = ['web']; + const remoteExtension = aLocalExtension('a', { extensionKind }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + + const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([]), createExtensionManagementService([remoteExtension])); + instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); + instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + testObject = await aWorkbenchService(); + + const actual = await testObject.queryLocal(); + + assert.equal(actual.length, 1); + assert.equal(actual[0].local, remoteExtension); + }); + async function aWorkbenchService(): Promise { const workbenchService: ExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); await workbenchService.queryLocal(); @@ -1031,4 +1411,49 @@ suite('ExtensionsWorkbenchServiceTest', () => { }); }); } + + function aMultiExtensionManagementServerService(instantiationService: TestInstantiationService, localExtensionManagementService?: IExtensionManagementService, remoteExtensionManagementService?: IExtensionManagementService): IExtensionManagementServerService { + const localExtensionManagementServer: IExtensionManagementServer = { + id: 'vscode-local', + label: 'local', + extensionManagementService: localExtensionManagementService || createExtensionManagementService() + }; + const remoteExtensionManagementServer: IExtensionManagementServer = { + id: 'vscode-remote', + label: 'remote', + extensionManagementService: remoteExtensionManagementService || createExtensionManagementService() + }; + return { + _serviceBrand: undefined, + localExtensionManagementServer, + remoteExtensionManagementServer, + webExtensionManagementServer: null, + getExtensionManagementServer: (extension: IExtension) => { + if (extension.location.scheme === Schemas.file) { + return localExtensionManagementServer; + } + if (extension.location.scheme === REMOTE_HOST_SCHEME) { + return remoteExtensionManagementServer; + } + throw new Error(''); + } + }; + } + + function createExtensionManagementService(installed: ILocalExtension[] = []): IExtensionManagementService { + return { + onInstallExtension: Event.None, + onDidInstallExtension: Event.None, + onUninstallExtension: Event.None, + onDidUninstallExtension: Event.None, + getInstalled: () => Promise.resolve(installed), + installFromGallery: (extension: IGalleryExtension) => Promise.reject(new Error('not supported')), + updateMetadata: async (local: ILocalExtension, metadata: IGalleryMetadata) => { + local.identifier.uuid = metadata.id; + local.publisherDisplayName = metadata.publisherDisplayName; + local.publisherId = metadata.publisherId; + return local; + } + }; + } }); diff --git a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts index 389bcae9802..1daf1517449 100644 --- a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts @@ -14,6 +14,9 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { openEditorWith } from 'vs/workbench/services/editor/common/editorOpenWith'; /** * An implementation of editor for binary files that cannot be displayed. @@ -27,6 +30,8 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { @IThemeService themeService: IThemeService, @IOpenerService private readonly openerService: IOpenerService, @IEditorService private readonly editorService: IEditorService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IQuickInputService private readonly quickInputService: IQuickInputService, @IStorageService storageService: IStorageService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, ) { @@ -46,8 +51,11 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { private async openInternal(input: EditorInput, options: EditorOptions | undefined): Promise { if (input instanceof FileEditorInput) { input.setForceOpenAsText(); - - await this.editorService.openEditor(input, options, this.group); + 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); + } } } diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 3461cc5eb0a..71f86a19285 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -34,7 +34,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExplorerService } from 'vs/workbench/contrib/files/common/explorerService'; -import { SUPPORTED_ENCODINGS } from 'vs/workbench/services/textfile/common/textfiles'; +import { SUPPORTED_ENCODINGS } from 'vs/workbench/services/textfile/common/encoding'; import { Schemas } from 'vs/base/common/network'; import { WorkspaceWatcher } from 'vs/workbench/contrib/files/common/workspaceWatcher'; import { editorConfigurationBaseNode } from 'vs/editor/common/config/commonEditorConfig'; diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 485601fdb5d..843ea8cd16f 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -678,13 +678,13 @@ class OpenEditorsDragAndDrop implements IListDragAndDrop { + elementsData.forEach((oe: OpenEditor, offset) => { oe.group.moveEditor(oe.editor, group, { index: index + offset, preserveFocus: true }); }); this.editorGroupService.activateGroup(group); diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index f856f4e05aa..26599aef2be 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts @@ -64,7 +64,7 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { DefaultFormatter.extensionDescriptions.push(nls.localize('nullFormatterDescription', "None")); for (const extension of extensions) { - if (extension.main) { + if (extension.main || extension.browser) { DefaultFormatter.extensionIds.push(extension.identifier.value); DefaultFormatter.extensionDescriptions.push(extension.description || ''); } diff --git a/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts b/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts index 439a4ba8eea..8799eee4c2d 100644 --- a/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts @@ -23,7 +23,7 @@ const workbenchActionsRegistry = Registry.as(Extension if (!!product.reportIssueUrl) { workbenchActionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ReportPerformanceIssueUsingReporterAction), 'Help: Report Performance Issue', helpCategory.value); - const OpenIssueReporterActionLabel = nls.localize({ key: 'reportIssueInEnglish', comment: ['Translate this to "Report Issue in English" in all languages please!'] }, "Report Issue"); + const OpenIssueReporterActionLabel = nls.localize({ key: 'reportIssueInEnglish', comment: ['Translate this to "Report Issue in English" in all languages please!'] }, "Report Issue..."); CommandsRegistry.registerCommand(OpenIssueReporterActionId, function (accessor, args?: [string] | OpenIssueReporterArgs) { const data: Partial = Array.isArray(args) diff --git a/src/vs/workbench/contrib/issue/electron-browser/issueService.ts b/src/vs/workbench/contrib/issue/electron-browser/issueService.ts index 0f06c778c4e..92dd4581ccc 100644 --- a/src/vs/workbench/contrib/issue/electron-browser/issueService.ts +++ b/src/vs/workbench/contrib/issue/electron-browser/issueService.ts @@ -15,6 +15,8 @@ import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/electron-brow import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { platform } from 'process'; +import { IProductService } from 'vs/platform/product/common/productService'; export class WorkbenchIssueService implements IWorkbenchIssueService { declare readonly _serviceBrand: undefined; @@ -24,7 +26,8 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { @IThemeService private readonly themeService: IThemeService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, - @IWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService + @IWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, + @IProductService private readonly productService: IProductService ) { } async openReporter(dataOverrides: Partial = {}): Promise { @@ -67,7 +70,9 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { hoverBackground: getColor(theme, listHoverBackground), hoverForeground: getColor(theme, listHoverForeground), highlightForeground: getColor(theme, listHighlightForeground), - } + }, + platform, + applicationName: this.productService.applicationName }; return this.issueService.openProcessExplorer(data); } diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index 5b3be0da40e..e737ad31f7f 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -45,7 +45,7 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { } private registerCommonContributions(): void { - this.registerLogChannel(Constants.userDataSyncLogChannelId, nls.localize('userDataSyncLog', "Preferences Sync"), this.environmentService.userDataSyncLogResource); + this.registerLogChannel(Constants.userDataSyncLogChannelId, nls.localize('userDataSyncLog', "Settings Sync"), this.environmentService.userDataSyncLogResource); this.registerLogChannel(Constants.rendererLogChannelId, nls.localize('rendererLog', "Window"), this.environmentService.logFile); } 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/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 4a8f6b2d4c2..45c6e3f2145 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -16,7 +16,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IDisposable, dispose, Disposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ActionBar, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { QuickFixAction, QuickFixActionViewItem } from 'vs/workbench/contrib/markers/browser/markersViewActions'; import { ILabelService } from 'vs/platform/label/common/label'; import { dirname, basename, isEqual } from 'vs/base/common/resources'; @@ -52,6 +52,7 @@ import { domEvent } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Progress } from 'vs/platform/progress/common/progress'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; export type TreeElement = ResourceMarkers | Marker | RelatedInformation; diff --git a/src/vs/workbench/contrib/markers/browser/markersView.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts index f6796e95edb..c7cff71ecf5 100644 --- a/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/markers'; import { URI } from 'vs/base/common/uri'; import * as dom from 'vs/base/browser/dom'; -import { IAction, IActionViewItem, Action } from 'vs/base/common/actions'; +import { IAction, IActionViewItem, Action, Separator } from 'vs/base/common/actions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import Constants from 'vs/workbench/contrib/markers/browser/constants'; @@ -33,7 +33,7 @@ import { deepClone } from 'vs/base/common/objects'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { FilterData, Filter, VirtualDelegate, ResourceMarkersRenderer, MarkerRenderer, RelatedInformationRenderer, TreeElement, MarkersTreeAccessibilityProvider, MarkersViewModel, ResourceDragAndDrop } from 'vs/workbench/contrib/markers/browser/markersTreeViewer'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { Separator, ActionViewItem, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IMenuService, MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; @@ -50,6 +50,7 @@ import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/vie import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { Codicon } from 'vs/base/common/codicons'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterable> { return Iterable.map(resourceMarkers.markers, m => { diff --git a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts index 8c75b1756db..48d031cea16 100644 --- a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts +++ b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts @@ -5,7 +5,7 @@ import { Delayer } from 'vs/base/common/async'; import * as DOM from 'vs/base/browser/dom'; -import { Action, IAction, IActionRunner } from 'vs/base/common/actions'; +import { Action, IAction, IActionRunner, Separator } from 'vs/base/common/actions'; import { HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; @@ -15,7 +15,7 @@ import Constants from 'vs/workbench/contrib/markers/browser/constants'; import { IThemeService, registerThemingParticipant, ICssStyleCollector, IColorTheme } from 'vs/platform/theme/common/themeService'; import { attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { toDisposable, Disposable } from 'vs/base/common/lifecycle'; -import { BaseActionViewItem, ActionViewItem, ActionBar, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { badgeBackground, badgeForeground, contrastBorder, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -23,10 +23,11 @@ import { ContextScopedHistoryInputBox } from 'vs/platform/browser/contextScopedH import { Marker } from 'vs/workbench/contrib/markers/browser/markersModel'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Event, Emitter } from 'vs/base/common/event'; -import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdown'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { IViewsService } from 'vs/workbench/common/views'; import { Codicon } from 'vs/base/common/codicons'; +import { BaseActionViewItem, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; export class ShowProblemsPanelAction extends Action { @@ -179,11 +180,12 @@ class FiltersDropdownMenuActionViewItem extends DropdownMenuActionViewItem { super(action, { getActions: () => this.getActions() }, contextMenuService, - action => undefined, - actionRunner!, - undefined, - action.class, - () => { return AnchorAlignment.RIGHT; }); + { + actionRunner, + classNames: action.class, + anchorAlignmentProvider: () => AnchorAlignment.RIGHT + } + ); } render(container: HTMLElement): void { diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts index cdc24904254..b6b383674be 100644 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ b/src/vs/workbench/contrib/notebook/browser/constants.ts @@ -13,11 +13,12 @@ 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 = 28; +export const BOTTOM_CELL_TOOLBAR_HEIGHT = 18; +export const BOTTOM_CELL_TOOLBAR_OFFSET = 12; export const CELL_STATUSBAR_HEIGHT = 22; // Margin above editor -export const EDITOR_TOP_MARGIN = 6; +export const CELL_TOP_MARGIN = 6; export const CELL_BOTTOM_MARGIN = 6; // Top and bottom padding inside the monaco editor in a cell, which are included in `cell.editorHeight` @@ -25,3 +26,5 @@ export const EDITOR_TOP_PADDING = 12; export const EDITOR_BOTTOM_PADDING = 4; export const CELL_OUTPUT_PADDING = 14; + +export const COLLAPSED_INDICATOR_HEIGHT = 24; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index 462c1a73fcd..0a0a8fb1f3f 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes'; +import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { URI } from 'vs/base/common/uri'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; @@ -18,20 +18,18 @@ 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, ICellViewModel, INotebookEditor, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_CELL_HAS_OUTPUTS, CellFocusMode, NOTEBOOK_OUTPUT_FOCUSED, NOTEBOOK_CELL_LIST_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, NotebookCellRunState, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +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 { 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'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; // Notebook Commands const EXECUTE_NOTEBOOK_COMMAND_ID = 'notebook.execute'; const CANCEL_NOTEBOOK_COMMAND_ID = 'notebook.cancelExecution'; const NOTEBOOK_FOCUS_TOP = 'notebook.focusTop'; const NOTEBOOK_FOCUS_BOTTOM = 'notebook.focusBottom'; -const NOTEBOOK_REDO = 'notebook.redo'; -const NOTEBOOK_UNDO = 'notebook.undo'; const NOTEBOOK_FOCUS_PREVIOUS_EDITOR = 'notebook.focusPreviousEditor'; const NOTEBOOK_FOCUS_NEXT_EDITOR = 'notebook.focusNextEditor'; const CLEAR_ALL_CELLS_OUTPUTS_COMMAND_ID = 'notebook.clearAllCellsOutputs'; @@ -72,9 +70,14 @@ const CENTER_ACTIVE_CELL = 'notebook.centerActiveCell'; const FOCUS_IN_OUTPUT_COMMAND_ID = 'notebook.cell.focusInOutput'; const FOCUS_OUT_OUTPUT_COMMAND_ID = 'notebook.cell.focusOutOutput'; +const COLLAPSE_CELL_INPUT_COMMAND_ID = 'notebook.cell.collapseCellContent'; +const COLLAPSE_CELL_OUTPUT_COMMAND_ID = 'notebook.cell.collapseCellOutput'; +const EXPAND_CELL_OUTPUT_COMMAND_ID = 'notebook.cell.expandCellOutput'; + export const NOTEBOOK_ACTIONS_CATEGORY = { value: localize('notebookActions.category', "Notebook"), original: 'Notebook' }; -export const CELL_TITLE_GROUP_ID = 'inline'; +export const CELL_TITLE_CELL_GROUP_ID = 'inline/cell'; +export const CELL_TITLE_OUTPUT_GROUP_ID = 'inline/output'; const EDITOR_WIDGET_ACTION_WEIGHT = KeybindingWeight.EditorContrib; // smaller than Suggest Widget, etc @@ -402,7 +405,12 @@ registerAction2(class extends NotebookCellAction { primary: KeyCode.KEY_Y, weight: KeybindingWeight.WorkbenchContrib }, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR), + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_CELL_TYPE.isEqualTo('markdown')), + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_TYPE.isEqualTo('markdown')), + group: '2_edit', + } }); } @@ -421,6 +429,12 @@ registerAction2(class extends NotebookCellAction { primary: KeyCode.KEY_M, weight: KeybindingWeight.WorkbenchContrib }, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_CELL_TYPE.isEqualTo('code')), + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_TYPE.isEqualTo('code')), + group: '2_edit', + } }); } @@ -494,7 +508,7 @@ abstract class InsertCellCommand extends NotebookAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { - const newCell = context.notebookEditor.insertNotebookCell(context.cell, this.kind, this.direction, undefined, context.ui); + const newCell = context.notebookEditor.insertNotebookCell(context.cell, this.kind, this.direction, undefined, true); if (newCell) { context.notebookEditor.focusNotebookCell(newCell, 'editor'); } @@ -597,7 +611,7 @@ registerAction2(class extends NotebookCellAction { NOTEBOOK_CELL_MARKDOWN_EDIT_MODE.toNegated(), NOTEBOOK_CELL_EDITABLE), order: CellToolbarOrder.EditCell, - group: CELL_TITLE_GROUP_ID + group: CELL_TITLE_CELL_GROUP_ID }, icon: { id: 'codicon/pencil' } }); @@ -621,7 +635,7 @@ registerAction2(class extends NotebookCellAction { NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_EDITABLE), order: CellToolbarOrder.SaveCell, - group: CELL_TITLE_GROUP_ID + group: CELL_TITLE_CELL_GROUP_ID }, icon: { id: 'codicon/check' }, keybinding: { @@ -655,7 +669,7 @@ registerAction2(class extends NotebookCellAction { id: MenuId.NotebookCellTitle, order: CellToolbarOrder.DeleteCell, when: NOTEBOOK_EDITOR_EDITABLE, - group: CELL_TITLE_GROUP_ID + group: CELL_TITLE_CELL_GROUP_ID }, keybinding: { primary: KeyCode.Delete, @@ -749,11 +763,11 @@ registerAction2(class extends NotebookCellAction { { id: COPY_CELL_COMMAND_ID, title: localize('notebookActions.copy', "Copy Cell"), - keybinding: { - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), - primary: KeyMod.CtrlCmd | KeyCode.KEY_C, - weight: EDITOR_WIDGET_ACTION_WEIGHT - }, + menu: { + id: MenuId.NotebookCellTitle, + when: NOTEBOOK_EDITOR_FOCUSED, + group: '1_copy', + } }); } @@ -771,11 +785,11 @@ registerAction2(class extends NotebookCellAction { { id: CUT_CELL_COMMAND_ID, title: localize('notebookActions.cut', "Cut Cell"), - keybinding: { - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), - primary: KeyMod.CtrlCmd | KeyCode.KEY_X, - weight: EDITOR_WIDGET_ACTION_WEIGHT - }, + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), + group: '1_copy', + } }); } @@ -785,7 +799,7 @@ registerAction2(class extends NotebookCellAction { clipboardService.writeText(context.cell.getText()); const viewModel = context.notebookEditor.viewModel; - if (!viewModel) { + if (!viewModel || !viewModel.metadata.editable) { return; } @@ -794,72 +808,17 @@ registerAction2(class extends NotebookCellAction { } }); -registerAction2(class extends NotebookCellAction { - constructor() { - super( - { - id: PASTE_CELL_ABOVE_COMMAND_ID, - title: localize('notebookActions.pasteAbove', "Paste Cell Above"), - keybinding: { - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_V, - weight: EDITOR_WIDGET_ACTION_WEIGHT - }, - }); - } - - async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - const notebookService = accessor.get(INotebookService); - const pasteCells = notebookService.getToCopy(); - - const viewModel = context.notebookEditor.viewModel; - - if (!viewModel) { - return; - } - - if (!pasteCells) { - return; - } - - const currCellIndex = viewModel.getCellIndex(context!.cell); - - 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(currCellIndex, pasteCell, true); - return; - }); - - if (topPastedCell) { - context.notebookEditor.focusNotebookCell(topPastedCell, 'container'); - } - } -}); registerAction2(class extends NotebookAction { constructor() { super( { id: PASTE_CELL_COMMAND_ID, title: localize('notebookActions.paste', "Paste Cell"), - keybinding: { - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), - primary: KeyMod.CtrlCmd | KeyCode.KEY_V, - weight: EDITOR_WIDGET_ACTION_WEIGHT - }, + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE), + group: '1_copy', + } }); } @@ -869,7 +828,7 @@ registerAction2(class extends NotebookAction { const viewModel = context.notebookEditor.viewModel; - if (!viewModel) { + if (!viewModel || !viewModel.metadata.editable) { return; } @@ -905,6 +864,62 @@ registerAction2(class extends NotebookAction { } }); +registerAction2(class extends NotebookCellAction { + constructor() { + super( + { + id: PASTE_CELL_ABOVE_COMMAND_ID, + title: localize('notebookActions.pasteAbove', "Paste Cell Above"), + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_V, + weight: EDITOR_WIDGET_ACTION_WEIGHT + }, + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { + const notebookService = accessor.get(INotebookService); + const pasteCells = notebookService.getToCopy(); + + const viewModel = context.notebookEditor.viewModel; + + if (!viewModel || !viewModel.metadata.editable) { + return; + } + + if (!pasteCells) { + return; + } + + const currCellIndex = viewModel.getCellIndex(context!.cell); + + 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(currCellIndex, pasteCell, true); + return; + }); + + if (topPastedCell) { + context.notebookEditor.focusNotebookCell(topPastedCell, 'container'); + } + } +}); + registerAction2(class extends NotebookCellAction { constructor() { super( @@ -1077,42 +1092,6 @@ registerAction2(class extends NotebookCellAction { }); -registerAction2(class extends NotebookAction { - constructor() { - super({ - id: NOTEBOOK_UNDO, - title: localize('undo', 'Undo'), - keybinding: { - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), - primary: KeyMod.CtrlCmd | KeyCode.KEY_Z, - weight: KeybindingWeight.WorkbenchContrib - } - }); - } - - async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { - await context.notebookEditor.viewModel?.undo(); - } -}); - -registerAction2(class extends NotebookAction { - constructor() { - super({ - id: NOTEBOOK_REDO, - title: localize('redo', 'Redo'), - keybinding: { - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z, - weight: KeybindingWeight.WorkbenchContrib - } - }); - } - - async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { - await context.notebookEditor.viewModel?.redo(); - } -}); - registerAction2(class extends NotebookAction { constructor() { super({ @@ -1172,7 +1151,7 @@ registerAction2(class extends NotebookCellAction { id: MenuId.NotebookCellTitle, when: ContextKeyExpr.and(NOTEBOOK_CELL_TYPE.isEqualTo('code'), NOTEBOOK_EDITOR_RUNNABLE), order: CellToolbarOrder.ClearCellOutput, - group: CELL_TITLE_GROUP_ID + group: CELL_TITLE_OUTPUT_GROUP_ID }, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey), NOTEBOOK_CELL_HAS_OUTPUTS), @@ -1319,11 +1298,9 @@ registerAction2(class extends NotebookAction { }); async function splitCell(context: INotebookCellActionContext): Promise { - if (context.cell.cellKind === CellKind.Code) { - const newCells = await context.notebookEditor.splitNotebookCell(context.cell); - if (newCells) { - context.notebookEditor.focusNotebookCell(newCells[newCells.length - 1], 'editor'); - } + const newCells = await context.notebookEditor.splitNotebookCell(context.cell); + if (newCells) { + context.notebookEditor.focusNotebookCell(newCells[newCells.length - 1], 'editor'); } } @@ -1335,13 +1312,17 @@ registerAction2(class extends NotebookCellAction { title: localize('notebookActions.splitCell', "Split Cell"), menu: { id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_CELL_TYPE.isEqualTo('code'), NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, InputFocusedContext), + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, InputFocusedContext), order: CellToolbarOrder.SplitCell, - group: CELL_TITLE_GROUP_ID + group: CELL_TITLE_CELL_GROUP_ID, + // alt: { + // id: JOIN_CELL_BELOW_COMMAND_ID, + // title: localize('notebookActions.joinCellBelow', "Join with Next Cell") + // } }, icon: { id: 'codicon/split-vertical' }, keybinding: { - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_CELL_TYPE.isEqualTo('code'), NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, InputFocusedContext), + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, InputFocusedContext), primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_BACKSLASH), weight: KeybindingWeight.WorkbenchContrib }, @@ -1355,7 +1336,7 @@ registerAction2(class extends NotebookCellAction { async function joinCells(context: INotebookCellActionContext, direction: 'above' | 'below'): Promise { - const cell = await context.notebookEditor.joinNotebookCells(context.cell, direction, CellKind.Code); + const cell = await context.notebookEditor.joinNotebookCells(context.cell, direction); if (cell) { context.notebookEditor.focusNotebookCell(cell, 'editor'); } @@ -1366,7 +1347,7 @@ registerAction2(class extends NotebookCellAction { super( { id: JOIN_CELL_ABOVE_COMMAND_ID, - title: localize('notebookActions.joinCellAbove', "Join with Previous Cell"), + title: localize('notebookActions.joinCellAbove', "Join With Previous Cell"), keybinding: { when: NOTEBOOK_EDITOR_FOCUSED, primary: KeyMod.WinCtrl | KeyMod.Alt | KeyMod.Shift | KeyCode.KEY_J, @@ -1385,11 +1366,16 @@ registerAction2(class extends NotebookCellAction { super( { id: JOIN_CELL_BELOW_COMMAND_ID, - title: localize('notebookActions.joinCellBelow', "Join with Next Cell"), + title: localize('notebookActions.joinCellBelow', "Join With Next Cell"), keybinding: { when: NOTEBOOK_EDITOR_FOCUSED, primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.KEY_J, weight: KeybindingWeight.WorkbenchContrib + }, + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), + group: '2_edit', } }); } @@ -1419,3 +1405,129 @@ registerAction2(class extends NotebookCellAction { return context.notebookEditor.revealInCenter(context.cell); } }); + +registerAction2(class extends NotebookCellAction { + constructor() { + super({ + id: COLLAPSE_CELL_INPUT_COMMAND_ID, + 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), + weight: KeybindingWeight.WorkbenchContrib + }, + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_INPUT_COLLAPSED.toNegated()), + group: '3_collapse', + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { inputCollapsed: true }); + } +}); + +registerAction2(class extends NotebookCellAction { + constructor() { + super({ + id: EXPAND_CELL_CONTENT_COMMAND_ID, + 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), + weight: KeybindingWeight.WorkbenchContrib + }, + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_INPUT_COLLAPSED), + group: '3_collapse', + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { inputCollapsed: false }); + } +}); + +registerAction2(class extends NotebookCellAction { + constructor() { + super({ + id: COLLAPSE_CELL_OUTPUT_COMMAND_ID, + 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), + 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', + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { outputCollapsed: true }); + } +}); + +registerAction2(class extends NotebookCellAction { + constructor() { + super({ + id: EXPAND_CELL_OUTPUT_COMMAND_ID, + 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), + weight: KeybindingWeight.WorkbenchContrib + }, + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED), + group: '3_collapse', + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { outputCollapsed: false }); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'notebook.inspectLayout', + title: localize('notebookActions.inspectLayout', "Inspect Notebook Layout"), + category: { value: localize({ key: 'developer', comment: ['A developer on Code itself or someone diagnosing issues in Code'] }, "Developer"), original: 'Developer' }, + f1: true + }); + } + + protected getActiveEditorContext(accessor: ServicesAccessor): INotebookActionContext | undefined { + const editorService = accessor.get(IEditorService); + + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + const activeCell = editor.getActiveCell(); + return { + cell: activeCell, + notebookEditor: editor + }; + } + + run(accessor: ServicesAccessor) { + const activeEditorContext = this.getActiveEditorContext(accessor); + + if (activeEditorContext) { + activeEditorContext.notebookEditor.viewModel!.inspectLayout(); + } + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts index e4525a00a52..71f74948655 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts @@ -27,6 +27,7 @@ import { getActiveNotebookEditor } from 'vs/workbench/contrib/notebook/browser/c import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { EditorStartFindAction, EditorStartFindReplaceAction } from 'vs/editor/contrib/find/findController'; const FIND_HIDE_TRANSITION = 'find-hide-transition'; const FIND_SHOW_TRANSITION = 'find-show-transition'; @@ -243,13 +244,13 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote private setAllFindMatchesDecorations(cellFindMatches: CellFindMatch[]) { this._notebookEditor.changeModelDecorations((accessor) => { - let findMatchesOptions: ModelDecorationOptions = FindDecorations._FIND_MATCH_DECORATION; + const findMatchesOptions: ModelDecorationOptions = FindDecorations._FIND_MATCH_DECORATION; - let deltaDecorations: ICellModelDeltaDecorations[] = cellFindMatches.map(cellFindMatch => { + const deltaDecorations: ICellModelDeltaDecorations[] = cellFindMatches.map(cellFindMatch => { const findMatches = cellFindMatch.matches; // Find matches - let newFindMatchesDecorations: IModelDeltaDecoration[] = new Array(findMatches.length); + const newFindMatchesDecorations: IModelDeltaDecoration[] = new Array(findMatches.length); for (let i = 0, len = findMatches.length; i < len; i++) { newFindMatchesDecorations[i] = { range: findMatches[i].range, @@ -285,6 +286,27 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote } } + replace(initialFindInput?: string, initialReplaceInput?: string) { + super.showWithReplace(initialFindInput, initialReplaceInput); + this._replaceInput.select(); + + if (this._showTimeout === null) { + if (this._hideTimeout !== null) { + window.clearTimeout(this._hideTimeout); + this._hideTimeout = null; + this._notebookEditor.removeClassName(FIND_HIDE_TRANSITION); + } + + this._notebookEditor.addClassName(FIND_SHOW_TRANSITION); + this._showTimeout = window.setTimeout(() => { + this._notebookEditor.removeClassName(FIND_SHOW_TRANSITION); + this._showTimeout = null; + }, 200); + } else { + // no op + } + } + hide() { super.hide(); this.set([], false); @@ -333,8 +355,8 @@ registerAction2(class extends Action2 { } async run(accessor: ServicesAccessor): Promise { - let editorService = accessor.get(IEditorService); - let editor = getActiveNotebookEditor(editorService); + const editorService = accessor.get(IEditorService); + const editor = getActiveNotebookEditor(editorService); if (!editor) { return; @@ -360,8 +382,8 @@ registerAction2(class extends Action2 { } async run(accessor: ServicesAccessor): Promise { - let editorService = accessor.get(IEditorService); - let editor = getActiveNotebookEditor(editorService); + const editorService = accessor.get(IEditorService); + const editor = getActiveNotebookEditor(editorService); if (!editor) { return; @@ -371,3 +393,29 @@ registerAction2(class extends Action2 { controller.show(); } }); + +EditorStartFindAction.addImplementation(100, (accessor: ServicesAccessor, args: any) => { + const editorService = accessor.get(IEditorService); + const editor = getActiveNotebookEditor(editorService); + + if (!editor) { + return false; + } + + const controller = editor.getContribution(NotebookFindWidget.id); + controller.show(); + return true; +}); + +EditorStartFindReplaceAction.addImplementation(100, (accessor: ServicesAccessor, args: any) => { + const editorService = accessor.get(IEditorService); + const editor = getActiveNotebookEditor(editorService); + + if (!editor) { + return false; + } + + const controller = editor.getContribution(NotebookFindWidget.id); + controller.replace(); + return true; +}); 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 db590365fd4..814dc3a2527 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts @@ -140,7 +140,8 @@ registerAction2(class extends Action2 { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_OPEN_SQUARE_BRACKET, mac: { - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_OPEN_SQUARE_BRACKET + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_OPEN_SQUARE_BRACKET, + secondary: [KeyCode.LeftArrow], }, secondary: [KeyCode.LeftArrow], weight: KeybindingWeight.WorkbenchContrib @@ -183,7 +184,8 @@ registerAction2(class extends Action2 { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_CLOSE_SQUARE_BRACKET, mac: { - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_CLOSE_SQUARE_BRACKET + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_CLOSE_SQUARE_BRACKET, + secondary: [KeyCode.RightArrow], }, secondary: [KeyCode.RightArrow], weight: KeybindingWeight.WorkbenchContrib 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 d612cf6e87a..18f853b05e5 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts @@ -79,7 +79,7 @@ export class FoldingModel extends Disposable { recompute() { const cells = this._viewModel!.viewCells; - let stack: { index: number, level: number, endIndex: number }[] = []; + const stack: { index: number, level: number, endIndex: number }[] = []; for (let i = 0; i < cells.length; i++) { const cell = cells[i]; @@ -129,9 +129,9 @@ export class FoldingModel extends Disposable { // restore collased state let i = 0; - let nextCollapsed = () => { + const nextCollapsed = () => { while (i < this._regions.length) { - let isCollapsed = this._regions.isCollapsed(i); + const isCollapsed = this._regions.isCollapsed(i); i++; if (isCollapsed) { return i - 1; @@ -145,12 +145,12 @@ export class FoldingModel extends Disposable { while (collapsedIndex !== -1 && k < newRegions.length) { // get the latest range - let decRange = this._viewModel!.getTrackedRange(this._foldingRangeDecorationIds[collapsedIndex]); + const decRange = this._viewModel!.getTrackedRange(this._foldingRangeDecorationIds[collapsedIndex]); if (decRange) { - let collasedStartIndex = decRange.start; + const collasedStartIndex = decRange.start; while (k < newRegions.length) { - let startIndex = newRegions.getStartLineNumber(k) - 1; + const startIndex = newRegions.getStartLineNumber(k) - 1; if (collasedStartIndex >= startIndex) { newRegions.setCollapsed(k, collasedStartIndex === startIndex); k++; @@ -186,7 +186,7 @@ export class FoldingModel extends Disposable { const collapsedRanges: ICellRange[] = []; let i = 0; while (i < this._regions.length) { - let isCollapsed = this._regions.isCollapsed(i); + const isCollapsed = this._regions.isCollapsed(i); if (isCollapsed) { const region = this._regions.toRegion(i); @@ -205,12 +205,12 @@ export class FoldingModel extends Disposable { while (k < state.length && i < this._regions.length) { // get the latest range - let decRange = this._viewModel!.getTrackedRange(this._foldingRangeDecorationIds[i]); + const decRange = this._viewModel!.getTrackedRange(this._foldingRangeDecorationIds[i]); if (decRange) { - let collasedStartIndex = state[k].start; + const collasedStartIndex = state[k].start; while (i < this._regions.length) { - let startIndex = this._regions.getStartLineNumber(i) - 1; + const startIndex = this._regions.getStartLineNumber(i) - 1; if (collasedStartIndex >= startIndex) { this._regions.setCollapsed(i, collasedStartIndex === startIndex); i++; 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 a944b34daff..6cb8b4a2bb0 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts @@ -65,7 +65,7 @@ registerAction2(class extends Action2 { const edits: WorkspaceTextEdit[] = []; - for (let cell of notebook.cells) { + for (const cell of notebook.cells) { const ref = await textModelService.createModelReference(cell.uri); dispoables.add(ref); 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 e3bedbffbcd..bb6be90bb17 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts @@ -19,6 +19,8 @@ import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWo import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/common/statusbar'; +import { NotebookKernelProviderAssociation, NotebookKernelProviderAssociations, notebookKernelProviderAssociationsSettingId } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; registerAction2(class extends Action2 { @@ -37,6 +39,7 @@ registerAction2(class extends Action2 { const editorService = accessor.get(IEditorService); const notebookService = accessor.get(INotebookService); const quickInputService = accessor.get(IQuickInputService); + const configurationService = accessor.get(IConfigurationService); const activeEditorPane = editorService.activeEditorPane as unknown as { isNotebookEditor?: boolean } | undefined; if (!activeEditorPane?.isNotebookEditor) { @@ -48,7 +51,7 @@ 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, ...availableKernels].map((a) => { return { id: a.id, label: a.label, @@ -59,12 +62,17 @@ registerAction2(class extends Action2 { : a.extension.value + (a.id === activeKernel?.id ? nls.localize('currentActiveKernel', " (Currently Active)") : ''), + kernelProviderId: a.extension.value, run: async () => { editor.activeKernel = a; if ((a as any).resolve) { (a as INotebookKernelInfo2).resolve(editor.uri!, editor.getId(), tokenSource.token); } - } + }, + buttons: [{ + iconClass: 'codicon-settings-gear', + tooltip: nls.localize('notebook.promptKernel.setDefaultTooltip', "Set as default kernel provider for '{0}'", editor.viewModel!.viewType) + }] }; }); @@ -78,16 +86,61 @@ registerAction2(class extends Action2 { 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 action = await quickInputService.pick(picks, { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true }); - tokenSource.dispose(); - return action?.run(); + 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; })[]; + picker.placeholder = nls.localize('pickAction', "Select Action"); + picker.matchOnDetail = true; + const pickedItem = await new Promise<(IQuickPickItem & { run(): void; kernelProviderId?: string; }) | undefined>(resolve => { + picker.onDidAccept(() => { + resolve(picker.selectedItems.length === 1 ? picker.selectedItems[0] : undefined); + picker.dispose(); + }); + + picker.onDidTriggerItemButton(e => { + const pick = e.item; + const id = pick.id; + resolve(pick); // open the view + picker.dispose(); + + // And persist the setting + if (pick && id && pick.kernelProviderId) { + const newAssociation: NotebookKernelProviderAssociation = { viewType: editor.viewModel!.viewType, kernelProvider: pick.kernelProviderId }; + const currentAssociations = [...configurationService.getValue(notebookKernelProviderAssociationsSettingId)]; + + // First try updating existing association + for (let i = 0; i < currentAssociations.length; ++i) { + const existing = currentAssociations[i]; + if (existing.viewType === newAssociation.viewType) { + currentAssociations.splice(i, 1, newAssociation); + configurationService.updateValue(notebookKernelProviderAssociationsSettingId, currentAssociations); + return; + } + } + + // Otherwise, create a new one + currentAssociations.unshift(newAssociation); + configurationService.updateValue(notebookKernelProviderAssociationsSettingId, currentAssociations); + } + }); + + picker.show(); + }); + + tokenSource.dispose(); + return pickedItem?.run(); } }); @@ -114,8 +167,7 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { const activeEditor = getActiveNotebookEditor(this._editorService); - if (activeEditor && activeEditor.multipleKernelsAvailable) { - this.showKernelStatus(activeEditor.activeKernel); + if (activeEditor) { this._editorDisposable.add(activeEditor.onDidChangeKernel(() => { if (activeEditor.multipleKernelsAvailable) { this.showKernelStatus(activeEditor.activeKernel); @@ -123,6 +175,18 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { this.kernelInfoElement.clear(); } })); + + this._editorDisposable.add(activeEditor.onDidChangeAvailableKernels(() => { + if (activeEditor.multipleKernelsAvailable) { + this.showKernelStatus(activeEditor.activeKernel); + } else { + this.kernelInfoElement.clear(); + } + })); + } + + if (activeEditor && activeEditor.multipleKernelsAvailable) { + this.showKernelStatus(activeEditor.activeKernel); } else { this.kernelInfoElement.clear(); } diff --git a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts index 1f167a7a581..c7b5c94f98d 100644 --- a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts +++ b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts @@ -27,12 +27,14 @@ namespace NotebookRendererContribution { export const viewType = 'viewType'; export const displayName = 'displayName'; export const mimeTypes = 'mimeTypes'; + export const entrypoint = 'entrypoint'; } -interface INotebookRendererContribution { +export interface INotebookRendererContribution { readonly [NotebookRendererContribution.viewType]: string; readonly [NotebookRendererContribution.displayName]: string; readonly [NotebookRendererContribution.mimeTypes]?: readonly string[]; + readonly [NotebookRendererContribution.entrypoint]?: string; } const notebookProviderContribution: IJSONSchema = { @@ -115,7 +117,11 @@ const notebookRendererContribution: IJSONSchema = { items: { type: 'string' } - } + }, + [NotebookRendererContribution.entrypoint]: { + type: 'string', + description: nls.localize('contributes.notebook.renderer.entrypoint', 'File to load in the webview to render the extension.'), + }, } } }; diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 4d3d640acc7..80e04e4ae67 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -59,23 +59,37 @@ position: absolute; top: -500px; z-index: 1000; - padding-bottom: 8px; - padding-top: 8px; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-focus-indicator { +.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; +} + +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-drag-handle { display: none; } +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.code-cell-row .cell-focus-indicator-side { + height: 44px !important; +} + +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.code-cell-row .cell-focus-indicator-bottom { + top: 50px !important; +} + .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.markdown-cell-row .cell-focus-indicator { bottom: 8px; } +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.code-cell-row { + padding: 6px 0px; +} + .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .output { display: none !important; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image > .monaco-toolbar { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-title-toolbar { display: none; } @@ -84,8 +98,7 @@ } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-editor-part { - width: calc(100% - 32px); - /* minus left gutter */ + width: 100%; } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-editor-container > div > div { @@ -162,6 +175,7 @@ height: 16px; cursor: pointer; padding: 4px; + z-index: 27; } .monaco-workbench .notebookOverlay .output .error_message { @@ -197,13 +211,13 @@ .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu.mouseover, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .menu, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .menu { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-output-hover .menu { visibility: visible; } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-output-hover { outline: none !important; } @@ -211,6 +225,22 @@ outline: none !important; } +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-collapsed-part { + box-sizing: border-box; +} + +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-collapsed-part .codicon { + margin-top: 4px; + position: relative; + left: -23px; + cursor: pointer; +} + +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.collapsed .notebook-folding-indicator, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.collapsed .cell-title-toolbar { + display: none; +} + /* top and bottom borders on cells */ .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, @@ -258,7 +288,7 @@ cursor: pointer; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar { +.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; position: absolute; @@ -269,7 +299,7 @@ z-index: 30; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar .action-item { +.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; display: flex; @@ -277,17 +307,17 @@ margin: 1px 2px; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar .action-item .action-label { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar .action-item .action-label { display: flex; align-items: center; margin: auto; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar .action-item .monaco-dropdown { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar .action-item .monaco-dropdown { width: 100%; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar .action-item .monaco-dropdown .dropdown-label { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar .action-item .monaco-dropdown .dropdown-label { display: flex; } @@ -345,9 +375,10 @@ } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container { position: relative; - height: 22px; + height: 16px; flex-shrink: 0; top: 9px; + 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 { @@ -406,9 +437,9 @@ height: 2px; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions.focused > .monaco-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 > .monaco-toolbar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions:hover > .monaco-toolbar { +.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 .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; } @@ -418,12 +449,8 @@ } .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator { - display: block; - content: ' '; position: absolute; - box-sizing: border-box; top: 0px; - opacity: 0; } .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-side { @@ -441,7 +468,14 @@ right: 0px; } -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator:hover { +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-drag-handle { + position: absolute; + top: 0px; + z-index: 26; /* Above the bottom toolbar */ +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-drag-handle:hover, +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .cell-inner-container:hover { cursor: grab; } @@ -449,12 +483,6 @@ cursor: pointer; } -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row:hover .cell-focus-indicator, -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.cell-output-hover .cell-focus-indicator, -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator { - opacity: 1; -} - .monaco-workbench .notebookOverlay .monaco-list-row .cell-editor-part:before { z-index: 20; content: ""; @@ -481,16 +509,18 @@ z-index: 10; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list .monaco-list-row.cell-dragging { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list .monaco-list-row .cell-dragging { opacity: 0.5 !important; } .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; + justify-content: center; + z-index: 25; /* over the focus outline on the editor, below the title toolbar */ + width: 100%; opacity: 0; transition: opacity 0.2s ease-in-out; - cursor: auto; padding: 0; } @@ -531,19 +561,8 @@ align-items: center; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .separator { - height: 1px; - flex-grow: 1; - align-self: center; -} - -.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::after { - content: ' '; - display: block; - height: 1px; - width: 16px; - align-self: center; - margin: 0px 8px; +.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-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container span.codicon { @@ -739,3 +758,10 @@ .monaco-workbench.vs-dark .monaco-workbench .notebookOverlay .cell.markdown table > tbody > tr > td { border-color: rgba(255, 255, 255, 0.18); } */ + +.monaco-action-bar .action-item.verticalSeparator { + width: 1px !important; + height: 16px !important; + margin: 5px 4px !important; + cursor: none; +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index b73d6b5c81a..7a9953b2be8 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -30,7 +30,7 @@ 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, NotebookDocumentBackupData, NotebookEditorPriority } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellUri, getCellUndoRedoComparisonKey, NotebookDocumentBackupData, NotebookEditorPriority } 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'; @@ -151,12 +151,7 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri this._register(undoRedoService.registerUriComparisonKeyComputer(CellUri.scheme, { getComparisonKey: (uri: URI): string => { - const data = CellUri.parse(uri); - if (!data) { - return uri.toString(); - } - - return data.notebook.toString(); + return getCellUndoRedoComparisonKey(uri); } })); @@ -344,7 +339,7 @@ class CellContentProvider implements ITextModelContentProvider { const ref = await this._notebookModelResolverService.resolve(data.notebook, info.id); let result: ITextModel | null = null; - for (let cell of ref.object.notebook.cells) { + for (const cell of ref.object.notebook.cells) { if (cell.uri.toString() === resource.toString()) { const bufferFactory: ITextBufferFactory = { create: (defaultEOL) => { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 14a0da9f560..58fdb4b4a8e 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -8,7 +8,6 @@ import { 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'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ScrollEvent } from 'vs/base/common/scrollable'; @@ -20,13 +19,14 @@ 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, TimerRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; +import { CellLanguageStatusBarItem, 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 { 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'; export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); @@ -49,6 +49,11 @@ export const NOTEBOOK_CELL_RUNNABLE = new RawContextKey('notebookCellRu export const NOTEBOOK_CELL_MARKDOWN_EDIT_MODE = new RawContextKey('notebookCellMarkdownEditMode', false); // bool export const NOTEBOOK_CELL_RUN_STATE = new RawContextKey('notebookCellRunState', undefined); // idle, running export const NOTEBOOK_CELL_HAS_OUTPUTS = new RawContextKey('notebookCellHasOutputs', false); // bool +export const NOTEBOOK_CELL_INPUT_COLLAPSED = new RawContextKey('notebookCellInputIsCollapsed', false); // bool +export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey('notebookCellOutputIsCollapsed', false); // bool + +// Shared commands +export const EXPAND_CELL_CONTENT_COMMAND_ID = 'notebook.cell.expandCellContent'; // Kernels @@ -66,6 +71,13 @@ export interface NotebookLayoutChangeEvent { fontInfo?: boolean; } +export enum CodeCellLayoutState { + Uninitialized, + Estimated, + FromCache, + Measured +} + export interface CodeCellLayoutInfo { readonly fontInfo: BareFontInfo | null; readonly editorHeight: number; @@ -75,6 +87,7 @@ export interface CodeCellLayoutInfo { readonly outputTotalHeight: number; readonly indicatorHeight: number; readonly bottomToolbarOffset: number; + readonly layoutState: CodeCellLayoutState; } export interface CodeCellLayoutChangeEvent { @@ -109,7 +122,6 @@ export interface ICellViewModel { language: string; cellKind: CellKind; editState: CellEditState; - currentTokenSource: CancellationTokenSource | undefined; focusMode: CellFocusMode; getText(): string; getTextLength(): number; @@ -174,12 +186,14 @@ export interface INotebookEditor extends IEditor { isNotebookEditor: boolean; activeKernel: INotebookKernelInfo | INotebookKernelInfo2 | undefined; multipleKernelsAvailable: boolean; + readonly onDidChangeAvailableKernels: Event; readonly onDidChangeKernel: Event; isDisposed: boolean; getId(): string; getDomNode(): HTMLElement; + getOverflowContainerDomNode(): HTMLElement; getInnerWebview(): Webview | undefined; /** @@ -188,6 +202,9 @@ export interface INotebookEditor extends IEditor { focus(): void; hasFocus(): boolean; + hasWebviewFocus(): boolean; + + hasOutputTextSelection(): boolean; /** * Select & focus cell @@ -234,10 +251,16 @@ export interface INotebookEditor extends IEditor { moveCellDown(cell: ICellViewModel): Promise; /** + * @deprecated Note that this method doesn't support batch operations, use #moveCellToIdx instead. * Move a cell above or below another cell */ moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise; + /** + * Move a cell to a specific position + */ + moveCellToIdx(cell: ICellViewModel, index: number): Promise; + /** * Focus the container of a cell (the monaco editor inside is not focused). */ @@ -283,6 +306,11 @@ export interface INotebookEditor extends IEditor { */ removeInset(output: IProcessedOutput): void; + /** + * Hide the inset in the webview layer without removing it + */ + hideInset(output: IProcessedOutput): void; + /** * Send message to the webview for outputs. */ @@ -441,12 +469,15 @@ export interface INotebookCellList { } export interface BaseCellRenderTemplate { + editorPart: HTMLElement; + collapsedPart: HTMLElement; + expandButton: HTMLElement; contextKeyService: IContextKeyService; container: HTMLElement; cellContainer: HTMLElement; toolbar: ToolBar; betweenCellToolbar: ToolBar; - focusIndicator: HTMLElement; + focusIndicatorLeft: HTMLElement; disposables: DisposableStore; elementDisposables: DisposableStore; bottomCellContainer: HTMLElement; @@ -458,14 +489,13 @@ export interface BaseCellRenderTemplate { } export interface MarkdownCellRenderTemplate extends BaseCellRenderTemplate { - editorPart: HTMLElement; editorContainer: HTMLElement; foldingIndicator: HTMLElement; currentEditor?: ICodeEditor; } export interface CodeCellRenderTemplate extends BaseCellRenderTemplate { - cellRunStatusContainer: HTMLElement; + cellRunState: RunStateRenderer; cellStatusMessageContainer: HTMLElement; runToolbar: ToolBar; runButtonContainer: HTMLElement; @@ -477,6 +507,7 @@ export interface CodeCellRenderTemplate extends BaseCellRenderTemplate { timer: TimerRenderer; focusIndicatorRight: HTMLElement; focusIndicatorBottom: HTMLElement; + dragHandle: HTMLElement; } export function isCodeCellRenderTemplate(templateData: BaseCellRenderTemplate): templateData is CodeCellRenderTemplate { @@ -569,13 +600,13 @@ export function reduceCellRanges(_ranges: ICellRange[]): ICellRange[] { return []; } - let ranges = _ranges.sort((a, b) => a.start - b.start); - let result: ICellRange[] = []; + const ranges = _ranges.sort((a, b) => a.start - b.start); + const result: ICellRange[] = []; let currentRangeStart = ranges[0].start; let currentRangeEnd = ranges[0].end + 1; for (let i = 0, len = ranges.length; i < len; i++) { - let range = ranges[i]; + const range = ranges[i]; if (range.start > currentRangeEnd) { result.push({ start: currentRangeStart, end: currentRangeEnd - 1 }); @@ -597,7 +628,7 @@ export function getVisibleCells(cells: CellViewModel[], hiddenRanges: ICellRange let start = 0; let hiddenRangeIndex = 0; - let result: CellViewModel[] = []; + const result: CellViewModel[] = []; while (start < cells.length && hiddenRangeIndex < hiddenRanges.length) { if (start < hiddenRanges[hiddenRangeIndex].start) { @@ -614,3 +645,9 @@ export function getVisibleCells(cells: CellViewModel[], hiddenRanges: ICellRange return result; } + +export function getActiveNotebookEditor(editorService: IEditorService): INotebookEditor | undefined { + // TODO can `isNotebookEditor` be on INotebookEditor to avoid a circular dependency? + const activeEditorPane = editorService.activeEditorPane as unknown as { isNotebookEditor?: boolean } | undefined; + return activeEditorPane?.isNotebookEditor ? (editorService.activeEditorPane?.getControl() as INotebookEditor) : undefined; +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index f11c7c7878f..a954e866d9a 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -102,6 +102,7 @@ export class NotebookEditor extends BaseEditor { setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { super.setEditorVisible(visible, group); if (group) { + this._groupListener.clear(); this._groupListener.add(group.onWillCloseEditor(e => this._saveEditorViewState(e.editor))); this._groupListener.add(group.onDidGroupChange(() => { if (this._editorGroupService.activeGroup !== group) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts index ef987d7b89c..1df2b996c03 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts @@ -6,13 +6,14 @@ import { EditorInput, IEditorInput, GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions } from 'vs/workbench/common/editor'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { URI } from 'vs/base/common/uri'; -import { isEqual, basename } from 'vs/base/common/resources'; +import { isEqual, basename, joinPath } 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 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; interface NotebookEditorInputOptions { startDirty?: boolean; @@ -37,6 +38,7 @@ export class NotebookEditorInput extends EditorInput { @INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService, @IFilesConfigurationService private readonly _filesConfigurationService: IFilesConfigurationService, @IFileDialogService private readonly _fileDialogService: IFileDialogService, + @IPathService private readonly _pathService: IPathService, @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); @@ -108,7 +110,8 @@ export class NotebookEditorInput extends EditorInput { return undefined; } - const dialogPath = this._textModel.object.resource; + const dialogPath = this.isUntitled() ? await this.suggestName(this.name) : this._textModel.object.resource; + const target = await this._fileDialogService.pickFileToSave(dialogPath, options?.availableFileSystems); if (!target) { return undefined; // save cancelled @@ -136,6 +139,10 @@ ${patterns} return this._move(group, target)?.editor; } + async suggestName(suggestedFilename: string) { + return joinPath(this._fileDialogService.defaultFilePath() || (await this._pathService.userHome()), suggestedFilename); + } + // called when users rename a notebook document rename(group: GroupIdentifier, target: URI): IMoveResult | undefined { if (this._textModel) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index c9afd46a5ea..40d5380bd39 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -27,8 +27,8 @@ import { contrastBorder, editorBackground, focusBorder, foreground, registerColo 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, EDITOR_BOTTOM_PADDING, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING, SCROLLABLE_ELEMENT_PADDING_TOP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN } 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, NOTEBOOK_CELL_LIST_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +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 { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; @@ -37,7 +37,7 @@ import { CellDragAndDropController, CodeCellRenderer, MarkdownCellRenderer, Note 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 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, IProcessedOutput, INotebookKernelInfo, INotebookKernelInfoDto, INotebookKernelInfo2, NotebookRunState, NotebookCellRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -49,6 +49,8 @@ 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.$; @@ -67,12 +69,17 @@ export class NotebookEditorOptions extends EditorOptions { } } +const NotebookEditorActiveKernelCache = 'workbench.editor.notebook.activeKernel'; + export class NotebookEditorWidget extends Disposable implements INotebookEditor { static readonly ID: string = 'workbench.editor.notebook'; private static readonly EDITOR_MEMENTOS = new Map>(); private _overlayContainer!: HTMLElement; private _body!: HTMLElement; + private _overflowContainer!: HTMLElement; private _webview: BackLayerWebView | null = null; + private _webviewResolved: boolean = false; + private _webviewResolvePromise: Promise | null = null; private _webviewTransparentCover: HTMLElement | null = null; private _list: INotebookCellList | undefined; private _dndController: CellDragAndDropController | null = null; @@ -84,18 +91,17 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private _dimension: DOM.Dimension | null = null; private _shadowElementViewInfo: { height: number, width: number, top: number; left: number; } | null = null; - private _cellListFocusTracker: DOM.IFocusTracker | null = null; private _editorFocus: IContextKey | null = null; - private _cellListFocus: IContextKey | null = null; private _outputFocus: IContextKey | null = null; private _editorEditable: IContextKey | null = null; private _editorRunnable: IContextKey | null = null; - private _editorExecutingNotebook: IContextKey | null = null; + private _notebookExecuting: IContextKey | null = null; private _notebookHasMultipleKernels: IContextKey | null = null; private _outputRenderer: OutputRenderer; protected readonly _contributions: { [key: string]: INotebookEditorContribution; }; private _scrollBeyondLastLine: boolean; private readonly _memento: Memento; + private readonly _activeKernelMemento: Memento; private readonly _onDidFocusEmitter = this._register(new Emitter()); public readonly onDidFocus = this._onDidFocusEmitter.event; private _cellContextKeyManager: CellContextKeyManager | null = null; @@ -135,6 +141,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private _activeKernel: INotebookKernelInfo | INotebookKernelInfo2 | undefined = undefined; private readonly _onDidChangeKernel = this._register(new Emitter()); readonly onDidChangeKernel: Event = this._onDidChangeKernel.event; + private readonly _onDidChangeAvailableKernels = this._register(new Emitter()); + readonly onDidChangeAvailableKernels: Event = this._onDidChangeAvailableKernels.event; get activeKernel() { return this._activeKernel; @@ -145,12 +153,32 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } + if (this._activeKernel === kernel) { + return; + } + this._activeKernel = kernel; + this._activeKernelResolvePromise = undefined; + + const memento = this._activeKernelMemento.getMemento(StorageScope.GLOBAL); + memento[this.viewModel!.viewType] = this._activeKernel?.id; + this._activeKernelMemento.saveMemento(); this._onDidChangeKernel.fire(); } + private _activeKernelResolvePromise: Promise | undefined = undefined; + private _currentKernelTokenSource: CancellationTokenSource | undefined = undefined; - multipleKernelsAvailable: boolean = false; + private _multipleKernelsAvailable: boolean = false; + + get multipleKernelsAvailable() { + return this._multipleKernelsAvailable; + } + + set multipleKernelsAvailable(state: boolean) { + this._multipleKernelsAvailable = state; + this._onDidChangeAvailableKernels.fire(); + } private readonly _onDidChangeActiveEditor = this._register(new Emitter()); readonly onDidChangeActiveEditor: Event = this._onDidChangeActiveEditor.event; @@ -183,6 +211,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor ) { super(); this._memento = new Memento(NotebookEditorWidget.ID, storageService); + this._activeKernelMemento = new Memento(NotebookEditorActiveKernelCache, storageService); this._outputRenderer = new OutputRenderer(this, this.instantiationService); this._contributions = {}; @@ -237,12 +266,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor // Note - focus going to the webview will fire 'blur', but the webview element will be // a descendent of the notebook editor root. const focused = DOM.isAncestor(document.activeElement, this._overlayContainer); - if (focused) { - const cellListFocused = DOM.isAncestor(document.activeElement, this._body); - this._cellListFocus?.set(cellListFocused); - } else { - this._cellListFocus?.set(false); - } this._editorFocus?.set(focused); this._notebookViewModel?.setFocus(focused); } @@ -251,6 +274,45 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._editorFocus?.get() || false; } + hasWebviewFocus() { + return this._webiewFocused; + } + + hasOutputTextSelection() { + if (!this.hasFocus()) { + return false; + } + + const windowSelection = window.getSelection(); + if (windowSelection?.rangeCount !== 1) { + return false; + } + + const activeSelection = windowSelection.getRangeAt(0); + if (activeSelection.endOffset - activeSelection.startOffset === 0) { + return false; + } + + let container: any = activeSelection.commonAncestorContainer; + + if (!this._body.contains(container)) { + return false; + } + + while (container + && + container !== this._body) { + + if (DOM.hasClass(container as HTMLElement, 'output')) { + return true; + } + + container = container.parentNode; + } + + return false; + } + createEditor(): void { this._overlayContainer = document.createElement('div'); const id = generateUuid(); @@ -263,14 +325,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._createBody(this._overlayContainer); this._generateFontInfo(); this._editorFocus = NOTEBOOK_EDITOR_FOCUSED.bindTo(this.contextKeyService); - this._cellListFocus = NOTEBOOK_CELL_LIST_FOCUSED.bindTo(this.contextKeyService); this._isVisible = true; this._outputFocus = NOTEBOOK_OUTPUT_FOCUSED.bindTo(this.contextKeyService); this._editorEditable = NOTEBOOK_EDITOR_EDITABLE.bindTo(this.contextKeyService); this._editorEditable.set(true); this._editorRunnable = NOTEBOOK_EDITOR_RUNNABLE.bindTo(this.contextKeyService); this._editorRunnable.set(true); - this._editorExecutingNotebook = NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK.bindTo(this.contextKeyService); + this._notebookExecuting = NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK.bindTo(this.contextKeyService); this._notebookHasMultipleKernels = NOTEBOOK_HAS_MULTIPLE_KERNELS.bindTo(this.contextKeyService); this._notebookHasMultipleKernels.set(false); @@ -296,6 +357,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor DOM.addClass(this._body, 'cell-list-container'); this._createCellList(); DOM.append(parent, this._body); + + this._overflowContainer = document.createElement('div'); + DOM.addClass(this._overflowContainer, 'notebook-overflow-widget-container'); + DOM.addClass(this._overflowContainer, 'monaco-editor'); + DOM.append(parent, this._overflowContainer); } private _createCellList(): void { @@ -325,7 +391,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor multipleSelectionSupport: false, enableKeyboardNavigation: true, additionalScrollHeight: 0, - transformOptimization: false, + transformOptimization: true, styleController: (_suffix: string) => { return this._list!; }, overrideStyles: { listBackground: editorBackground, @@ -401,17 +467,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._register(widgetFocusTracker); this._register(widgetFocusTracker.onDidFocus(() => this._onDidFocusEmitter.fire())); - this._cellListFocusTracker = this._register(DOM.trackFocus(this._body)); - this._register(this._cellListFocusTracker.onDidFocus(() => { - // hack - FocusTracker forces 'blur' to run after 'focus'. - // We want the other way around so that when switching from notebook to notebook, the focus happens last - setTimeout(() => { - this.updateEditorFocus(); - }, 0); - })); - this._register(this._cellListFocusTracker.onDidBlur(() => { - this.updateEditorFocus(); - })); } private _updateForCursorNavigationMode(applyFocusChange: () => void): void { @@ -435,6 +490,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._overlayContainer; } + getOverflowContainerDomNode() { + return this._overflowContainer; + } + onWillHide() { this._isVisible = false; this._editorFocus?.set(false); @@ -484,14 +543,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._currentKernelTokenSource = new CancellationTokenSource(); this._localStore.add(this._currentKernelTokenSource); - await this._setKernels(textModel, this._currentKernelTokenSource); + // 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 () => { - if (this.activeKernel === undefined) { - this._currentKernelTokenSource?.cancel(); - this._currentKernelTokenSource = new CancellationTokenSource(); - await this._setKernels(textModel, this._currentKernelTokenSource); - } + this._currentKernelTokenSource?.cancel(); + this._currentKernelTokenSource = new CancellationTokenSource(); + await this._setKernels(textModel, this._currentKernelTokenSource); })); this._localStore.add(this._list!.onDidChangeFocus(() => { @@ -565,8 +623,17 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private async _setKernels(textModel: NotebookTextModel, tokenSource: CancellationTokenSource) { const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; const availableKernels2 = await this.notebookService.getContributedNotebookKernels2(textModel.viewType, textModel.uri, tokenSource.token); + + if (tokenSource.token.isCancellationRequested) { + 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; @@ -578,70 +645,192 @@ 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 - this._loadKernelPreloads(provider.providerExtensionLocation, provider.kernel); + await this._loadKernelPreloads(provider.providerExtensionLocation, provider.kernel); + tokenSource.dispose(); return; } - // choose a preferred kernel - const kernelsFromSameExtension = availableKernels2.filter(kernel => kernel.extension.value === provider.providerId); - if (kernelsFromSameExtension.length) { - const preferedKernel = kernelsFromSameExtension.find(kernel => kernel.isPreferred) || kernelsFromSameExtension[0]; - this.activeKernel = preferedKernel; - await preferedKernel.resolve(this.viewModel!.uri, this.getId(), tokenSource.token); + const activeKernelStillExist = [...availableKernels2, ...availableKernels].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 return; } + if (availableKernels2.length) { + return this._setKernelsFromProviders(provider, availableKernels2, tokenSource); + } // the provider doesn't have a builtin kernel, choose a kernel this.activeKernel = availableKernels[0]; if (this.activeKernel) { - this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); + await this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); } + + tokenSource.dispose(); } - private _loadKernelPreloads(extensionLocation: URI, kernel: INotebookKernelInfoDto) { - if (kernel.preloads) { + private async _setKernelsFromProviders(provider: NotebookProviderInfo, kernels: INotebookKernelInfo2[], tokenSource: CancellationTokenSource) { + const rawAssociations = this.configurationService.getValue(notebookKernelProviderAssociationsSettingId) || []; + const userSetKernelProvider = rawAssociations.filter(e => e.viewType === this.viewModel?.viewType)[0]?.kernelProvider; + const memento = this._activeKernelMemento.getMemento(StorageScope.GLOBAL); + + if (userSetKernelProvider) { + const filteredKernels = kernels.filter(kernel => kernel.extension.value === userSetKernelProvider); + + if (filteredKernels.length) { + const cachedKernelId = memento[provider.id]; + this.activeKernel = + filteredKernels.find(kernel => kernel.isPreferred) + || filteredKernels.find(kernel => kernel.id === cachedKernelId) + || filteredKernels[0]; + } else { + this.activeKernel = undefined; + } + + if (this.activeKernel) { + await this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); + + if (tokenSource.token.isCancellationRequested) { + return; + } + + this._activeKernelResolvePromise = this.activeKernel.resolve(this.viewModel!.uri, this.getId(), tokenSource.token); + await this._activeKernelResolvePromise; + + if (tokenSource.token.isCancellationRequested) { + return; + } + } + + memento[provider.id] = this._activeKernel?.id; + this._activeKernelMemento.saveMemento(); + + tokenSource.dispose(); + return; + } + + // choose a preferred kernel + const kernelsFromSameExtension = kernels.filter(kernel => kernel.extension.value === provider.providerExtensionId); + if (kernelsFromSameExtension.length) { + const cachedKernelId = memento[provider.id]; + + const preferedKernel = kernelsFromSameExtension.find(kernel => kernel.isPreferred) + || kernelsFromSameExtension.find(kernel => kernel.id === cachedKernelId) + || kernelsFromSameExtension[0]; + this.activeKernel = preferedKernel; + await this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); + + if (tokenSource.token.isCancellationRequested) { + return; + } + + await preferedKernel.resolve(this.viewModel!.uri, this.getId(), tokenSource.token); + + if (tokenSource.token.isCancellationRequested) { + return; + } + + memento[provider.id] = this._activeKernel?.id; + this._activeKernelMemento.saveMemento(); + tokenSource.dispose(); + return; + } + + // the provider doesn't have a builtin kernel, choose a kernel + this.activeKernel = kernels[0]; + if (this.activeKernel) { + await this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); + if (tokenSource.token.isCancellationRequested) { + return; + } + + await this.activeKernel.resolve(this.viewModel!.uri, this.getId(), tokenSource.token); + if (tokenSource.token.isCancellationRequested) { + return; + } + } + + tokenSource.dispose(); + } + + private async _loadKernelPreloads(extensionLocation: URI, kernel: INotebookKernelInfoDto) { + if (kernel.preloads && kernel.preloads.length) { + await this._resolveWebview(); this._webview?.updateKernelPreloads([extensionLocation], kernel.preloads.map(preload => URI.revive(preload))); } } private _updateForMetadata(): void { - this._editorEditable?.set(!!this.viewModel!.metadata?.editable); - this._editorRunnable?.set(!!this.viewModel!.metadata?.runnable); - DOM.toggleClass(this._overlayContainer, 'notebook-editor-editable', !!this.viewModel!.metadata?.editable); - DOM.toggleClass(this.getDomNode(), 'notebook-editor-editable', !!this.viewModel!.metadata?.editable); + const notebookMetadata = this.viewModel!.metadata; + this._editorEditable?.set(!!notebookMetadata?.editable); + this._editorRunnable?.set(!!notebookMetadata?.runnable); + DOM.toggleClass(this._overlayContainer, 'notebook-editor-editable', !!notebookMetadata?.editable); + DOM.toggleClass(this.getDomNode(), 'notebook-editor-editable', !!notebookMetadata?.editable); + + this._notebookExecuting?.set(notebookMetadata.runState === NotebookRunState.Running); + } + + private async _resolveWebview(): Promise { + if (!this.textModel) { + return null; + } + + if (this._webviewResolvePromise) { + return this._webviewResolvePromise; + } + + if (!this._webview) { + this._webview = this.instantiationService.createInstance(BackLayerWebView, this, this.getId(), this.textModel!.uri); + // attach the webview container to the DOM tree first + this._list?.rowsContainer.insertAdjacentElement('afterbegin', this._webview.element); + } + + this._webviewResolvePromise = new Promise(async resolve => { + await this._webview!.createWebview(); + this._webview!.webview!.onDidBlur(() => { + this._outputFocus?.set(false); + this.updateEditorFocus(); + + if (this._overlayContainer.contains(document.activeElement)) { + this._webiewFocused = false; + } + }); + this._webview!.webview!.onDidFocus(() => { + this._outputFocus?.set(true); + this.updateEditorFocus(); + this._onDidFocusEmitter.fire(); + + if (this._overlayContainer.contains(document.activeElement)) { + this._webiewFocused = true; + } + }); + + this._localStore.add(this._webview!.onMessage(({ message, forRenderer }) => { + if (this.viewModel) { + this.notebookService.onDidReceiveMessage(this.viewModel.viewType, this.getId(), forRenderer, message); + } + })); + + if (this.viewModel && this.viewModel!.renderers.size) { + this._webview?.updateRendererPreloads(this.viewModel!.renderers); + } + + this._webviewResolved = true; + + resolve(this._webview!); + }); + + return this._webviewResolvePromise; } private async _createWebview(id: string, resource: URI): Promise { this._webview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource); // attach the webview container to the DOM tree first this._list?.rowsContainer.insertAdjacentElement('afterbegin', this._webview.element); - await this._webview.createWebview(); - this._webview.webview.onDidBlur(() => { - this._outputFocus?.set(false); - this.updateEditorFocus(); - - if (this._overlayContainer.contains(document.activeElement)) { - this._webiewFocused = false; - } - }); - this._webview.webview.onDidFocus(() => { - this._outputFocus?.set(true); - this.updateEditorFocus(); - this._onDidFocusEmitter.fire(); - - if (this._overlayContainer.contains(document.activeElement)) { - this._webiewFocused = true; - } - }); - - this._localStore.add(this._webview.onMessage(({ message, forRenderer }) => { - if (this.viewModel) { - this.notebookService.onDidReceiveMessage(this.viewModel.viewType, this.getId(), forRenderer, message); - } - })); } private async _attachModel(textModel: NotebookTextModel, viewState: INotebookEditorViewState | undefined) { @@ -675,10 +864,17 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - this._webview?.updateRendererPreloads(this.viewModel.renderers); + if (this.viewModel.renderers.size) { + await this._resolveWebview(); + this._webview?.updateRendererPreloads(this.viewModel.renderers); + } this._localStore.add(this._list!.onWillScroll(e => { - this._webview!.updateViewScrollTop(-e.scrollTop, []); + if (!this._webviewResolved) { + return; + } + + this._webview?.updateViewScrollTop(-e.scrollTop, true, []); this._webviewTransparentCover!.style.top = `${e.scrollTop}px`; })); @@ -690,11 +886,16 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const scrollTop = this._list?.scrollTop || 0; const scrollHeight = this._list?.scrollHeight || 0; + + if (!this._webviewResolved) { + return; + } + this._webview!.element.style.height = `${scrollHeight}px`; if (this._webview?.insetMapping) { - let updateItems: { cell: CodeCellViewModel, output: IProcessedOutput, cellTop: number }[] = []; - let removedItems: IProcessedOutput[] = []; + const updateItems: { cell: CodeCellViewModel, output: IProcessedOutput, cellTop: number }[] = []; + const removedItems: IProcessedOutput[] = []; this._webview?.insetMapping.forEach((value, key) => { const cell = value.cell; const viewIndex = this._list?.getViewIndex(cell); @@ -721,7 +922,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor removedItems.forEach(output => this._webview?.removeInset(output)); if (updateItems.length) { - this._webview?.updateViewScrollTop(-scrollTop, updateItems); + this._webview?.updateViewScrollTop(-scrollTop, false, updateItems); } } }); @@ -735,7 +936,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.hideInset(output); })); - this._list!.layout(); + if (this._dimension) { + this._list?.layout(this._dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP, this._dimension.width); + } else { + this._list!.layout(); + } + this._dndController?.clearGlobalDragState(); // restore list state at last, it must be after list layout @@ -778,7 +984,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor if (this._list) { state.scrollPosition = { left: this._list.scrollLeft, top: this._list.scrollTop }; - let cellHeights: { [key: number]: number } = {}; + const cellHeights: { [key: number]: number } = {}; for (let i = 0; i < this.viewModel!.length; i++) { const elm = this.viewModel!.viewCells[i] as CellViewModel; if (elm.cellKind === CellKind.Code) { @@ -795,7 +1001,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const element = this._notebookViewModel!.viewCells[focus]; if (element) { const itemDOM = this._list?.domElementOfElement(element); - let editorFocused = !!(document.activeElement && itemDOM && itemDOM.contains(document.activeElement)); + const editorFocused = !!(document.activeElement && itemDOM && itemDOM.contains(document.activeElement)); state.editorFocused = editorFocused; state.focus = focus; @@ -951,7 +1157,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } - let relayout = (cell: ICellViewModel, height: number) => { + const relayout = (cell: ICellViewModel, height: number) => { if (this._isDisposed) { return; } @@ -1002,12 +1208,20 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } async splitNotebookCell(cell: ICellViewModel): Promise { + if (!this._notebookViewModel!.metadata.editable) { + return null; + } + const index = this._notebookViewModel!.getCellIndex(cell); return this._notebookViewModel!.splitNotebookCell(index); } async joinNotebookCells(cell: ICellViewModel, direction: 'above' | 'below', constraint?: CellKind): Promise { + if (!this._notebookViewModel!.metadata.editable) { + return null; + } + const index = this._notebookViewModel!.getCellIndex(cell); const ret = await this._notebookViewModel!.joinNotebookCells(index, direction, constraint); @@ -1048,7 +1262,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return null; } - const newIdx = index + 1; + 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); } @@ -1078,15 +1292,30 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const originalIdx = this._notebookViewModel!.getCellIndex(cell); const relativeToIndex = this._notebookViewModel!.getCellIndex(relativeToCell); - let newIdx = direction === 'above' ? relativeToIndex : relativeToIndex + 1; - if (originalIdx < newIdx) { - newIdx--; - } - + const newIdx = direction === 'above' ? relativeToIndex : relativeToIndex + 1; return this._moveCellToIndex(originalIdx, newIdx); } + async moveCellToIdx(cell: ICellViewModel, index: number): Promise { + if (!this._notebookViewModel!.metadata.editable) { + return null; + } + + const originalIdx = this._notebookViewModel!.getCellIndex(cell); + return this._moveCellToIndex(originalIdx, index); + } + + /** + * @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". + * @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) { + // The cell is moving "down", it will free up one index spot and consume a new one + newIdx--; + } + if (index === newIdx) { return null; } @@ -1120,7 +1349,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } getActiveCell() { - let elements = this._list?.getFocusedElements(); + const elements = this._list?.getFocusedElements(); if (elements && elements.length) { return elements[0]; @@ -1129,14 +1358,26 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return undefined; } - cancelNotebookExecution(): void { - if (!this._notebookViewModel!.currentTokenSource) { - throw new Error('Notebook is not executing'); + async cancelNotebookExecution(): Promise { + if (this._notebookViewModel?.metadata.runState !== NotebookRunState.Running) { + return; } + return this._cancelNotebookExecution(); + } - this._notebookViewModel!.currentTokenSource.cancel(); - this._notebookViewModel!.currentTokenSource = undefined; + 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); + } + } } async executeNotebook(): Promise { @@ -1147,46 +1388,58 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._executeNotebook(); } - async _executeNotebook(): Promise { - if (this._notebookViewModel!.currentTokenSource) { - return; - } + private async _executeNotebook(): Promise { + const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; + if (provider) { + const viewType = provider.id; + const notebookUri = this._notebookViewModel!.uri; - const tokenSource = new CancellationTokenSource(); - try { - this._editorExecutingNotebook!.set(true); - this._notebookViewModel!.currentTokenSource = tokenSource; - 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, undefined, tokenSource.token); - } else { - await this.notebookService.executeNotebook2(this._notebookViewModel!.viewType, this._notebookViewModel!.uri, this._activeKernel.id, tokenSource.token); + 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; } - } else if (provider.kernel) { - return await this.notebookService.executeNotebook(viewType, notebookUri, tokenSource.token); - } - } - } finally { - this._editorExecutingNotebook!.set(false); - this._notebookViewModel!.currentTokenSource = undefined; - tokenSource.dispose(); + await (this._activeKernel as INotebookKernelInfo2).executeNotebookCell!(this._notebookViewModel!.uri, undefined); + } else { + await this.notebookService.executeNotebook2(this._notebookViewModel!.viewType, this._notebookViewModel!.uri, this._activeKernel.id); + } + } else if (provider.kernel) { + return await this.notebookService.executeNotebook(viewType, notebookUri); + } } } - cancelNotebookCellExecution(cell: ICellViewModel): void { - if (!cell.currentTokenSource) { - throw new Error('Cell is not executing'); + async cancelNotebookCellExecution(cell: ICellViewModel): Promise { + if (cell.cellKind !== CellKind.Code) { + return; } - cell.currentTokenSource.cancel(); - cell.currentTokenSource = undefined; + const metadata = cell.getEvaluatedMetadata(this._notebookViewModel!.metadata); + if (!metadata.runnable) { + return; + } + + if (metadata.runState !== NotebookCellRunState.Running) { + return; + } + + await this._cancelNotebookCell(cell); + } + + 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); + } + } } async executeNotebookCell(cell: ICellViewModel): Promise { @@ -1199,37 +1452,26 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } - const tokenSource = new CancellationTokenSource(); - try { - await this._executeNotebookCell(cell, tokenSource); - } finally { - tokenSource.dispose(); - } + await this._executeNotebookCell(cell); } - private async _executeNotebookCell(cell: ICellViewModel, tokenSource: CancellationTokenSource): Promise { - try { - cell.currentTokenSource = tokenSource; + 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; - 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 { - 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, tokenSource.token); - } else { - - return await this.notebookService.executeNotebookCell2(viewType, notebookUri, cell.handle, this._activeKernel.id, tokenSource.token); - } - } else if (provider.kernel) { - return await this.notebookService.executeNotebookCell(viewType, notebookUri, cell.handle, tokenSource.token); + 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); } - } finally { - cell.currentTokenSource = undefined; } } @@ -1258,7 +1500,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor cell.focusMode = CellFocusMode.Container; this.revealInCenterIfOutsideViewport(cell); } else { - let itemDOM = this._list?.domElementOfElement(cell); + const itemDOM = this._list?.domElementOfElement(cell); if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { (document.activeElement as HTMLElement).blur(); } @@ -1305,21 +1547,23 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } - let preloads = this._notebookViewModel!.renderers; + await this._resolveWebview(); + + const preloads = this._notebookViewModel!.renderers; if (!this._webview!.insetMapping.has(output)) { - let cellTop = this._list?.getAbsoluteTopOfElement(cell) || 0; + const cellTop = this._list?.getAbsoluteTopOfElement(cell) || 0; await this._webview!.createInset(cell, output, cellTop, offset, shadowContent, preloads); } else { - let cellTop = this._list?.getAbsoluteTopOfElement(cell) || 0; - let scrollTop = this._list?.scrollTop || 0; + const cellTop = this._list?.getAbsoluteTopOfElement(cell) || 0; + const scrollTop = this._list?.scrollTop || 0; - this._webview!.updateViewScrollTop(-scrollTop, [{ cell: cell, output: output, cellTop: cellTop }]); + this._webview!.updateViewScrollTop(-scrollTop, true, [{ cell: cell, output: output, cellTop: cellTop }]); } } removeInset(output: IProcessedOutput) { - if (!this._webview) { + if (!this._webview || !this._webviewResolved) { return; } @@ -1327,7 +1571,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } hideInset(output: IProcessedOutput) { - if (!this._webview) { + if (!this._webview || !this._webviewResolved) { return; } @@ -1339,10 +1583,14 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } postMessage(forRendererId: string | undefined, message: any) { + if (!this._webview || !this._webviewResolved) { + return; + } + if (forRendererId === undefined) { - this._webview?.webview.postMessage(message); + this._webview.webview?.postMessage(message); } else { - this._webview?.postRendererMessage(forRendererId, message); + this._webview.postRendererMessage(forRendererId, message); } } @@ -1547,21 +1795,24 @@ registerThemingParticipant((theme, collector) => { const editorBackgroundColor = theme.getColor(editorBackground); if (editorBackgroundColor) { collector.addRule(`.notebookOverlay .cell-statusbar-container { border-top: solid 1px ${editorBackgroundColor}; }`); - collector.addRule(`.notebookOverlay .monaco-list-row > .monaco-toolbar { background-color: ${editorBackgroundColor}; }`); + 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} }`); } const cellToolbarSeperator = theme.getColor(CELL_TOOLBAR_SEPERATOR); if (cellToolbarSeperator) { - collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .separator { background-color: ${cellToolbarSeperator} }`); - collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .action-item:first-child::after { background-color: ${cellToolbarSeperator} }`); - collector.addRule(`.notebookOverlay .monaco-list-row > .monaco-toolbar { border: solid 1px ${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(`.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} }`); } const focusedCellBackgroundColor = theme.getColor(focusedCellBackground); if (focusedCellBackgroundColor) { collector.addRule(`.notebookOverlay .code-cell-row.focused .cell-focus-indicator, .notebookOverlay .markdown-cell-row.focused { background-color: ${focusedCellBackgroundColor} !important; }`); + collector.addRule(`.notebookOverlay .code-cell-row.focused .cell-collapsed-part { background-color: ${focusedCellBackgroundColor} !important; }`); } const cellHoverBackgroundColor = theme.getColor(cellHoverBackground); @@ -1569,6 +1820,8 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.notebookOverlay .code-cell-row:not(.focused):hover .cell-focus-indicator, .notebookOverlay .code-cell-row:not(.focused).cell-output-hover .cell-focus-indicator, .notebookOverlay .markdown-cell-row:not(.focused):hover { background-color: ${cellHoverBackgroundColor} !important; }`); + collector.addRule(`.notebookOverlay .code-cell-row:not(.focused):hover .cell-collapsed-part, + .notebookOverlay .code-cell-row:not(.focused).cell-output-hover .cell-collapsed-part { background-color: ${cellHoverBackgroundColor}; }`); } const focusedCellBorderColor = theme.getColor(focusedCellBorder); @@ -1581,25 +1834,21 @@ registerThemingParticipant((theme, collector) => { const cellSymbolHighlightColor = theme.getColor(cellSymbolHighlight); if (cellSymbolHighlightColor) { - collector.addRule(`.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.nb-symbolHighlight .cell-focus-indicator, - .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.nb-symbolHighlight.markdown-cell-row { + collector.addRule(`.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row .nb-symbolHighlight .cell-focus-indicator, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .nb-symbolHighlight { background-color: ${cellSymbolHighlightColor} !important; }`); } const focusedEditorBorderColorColor = theme.getColor(focusedEditorBorderColor); if (focusedEditorBorderColorColor) { - collector.addRule(`.notebookOverlay .monaco-list-row.cell-editor-focus .cell-editor-part:before { outline: solid 1px ${focusedEditorBorderColorColor}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row .cell-editor-focus .cell-editor-part:before { outline: solid 1px ${focusedEditorBorderColorColor}; }`); } - const editorBorderColor = theme.getColor(notebookCellBorder); - if (editorBorderColor) { - collector.addRule(`.notebookOverlay .monaco-list-row .cell-editor-part:before { outline: solid 1px ${editorBorderColor}; }`); - } - - const headingBorderColor = theme.getColor(notebookCellBorder); - if (headingBorderColor) { - collector.addRule(`.notebookOverlay .cell.markdown h1 { border-color: ${headingBorderColor}; }`); + const cellBorderColor = theme.getColor(notebookCellBorder); + if (cellBorderColor) { + collector.addRule(`.notebookOverlay .cell.markdown h1 { border-color: ${cellBorderColor}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row .cell-editor-part:before { outline: solid 1px ${cellBorderColor}; }`); } const cellStatusSuccessIcon = theme.getColor(cellStatusIconSuccess); @@ -1646,23 +1895,23 @@ registerThemingParticipant((theme, collector) => { } // 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 { padding-top: ${EDITOR_TOP_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row { padding-bottom: ${CELL_BOTTOM_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row .cell-bottom-toolbar-container { margin-top: ${CELL_BOTTOM_MARGIN}px; }`); + 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 > .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-bottom-toolbar-container { width: calc(100% - ${CELL_MARGIN * 2 + CELL_RUN_GUTTER}px); margin: 0px ${CELL_MARGIN * 2}px 0px ${CELL_MARGIN + CELL_RUN_GUTTER}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: ${CELL_RUN_GUTTER}px; }`); - collector.addRule(`.notebookOverlay .cell-drag-image .cell-editor-container > div { padding: ${EDITOR_TOP_PADDING}px 16px ${EDITOR_BOTTOM_PADDING}px 16px; }`); - collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top { height: ${EDITOR_TOP_MARGIN}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.code-cell-row .cell-focus-indicator-left { width: ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}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; }`); collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator.cell-focus-indicator-right { width: ${CELL_MARGIN * 2}px; }`); collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom { height: ${CELL_BOTTOM_MARGIN}px; }`); 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; }`); }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts index 2369eee6a02..def8c4e957a 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts @@ -68,7 +68,7 @@ class NotebookEditorWidgetService implements INotebookEditorWidgetService { const widgets = this._notebookWidgets.get(group.id); this._notebookWidgets.delete(group.id); if (widgets) { - for (let value of widgets.values()) { + for (const value of widgets.values()) { value.token = undefined; this._disposeWidget(value.widget); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookKernelAssociation.ts b/src/vs/workbench/contrib/notebook/browser/notebookKernelAssociation.ts new file mode 100644 index 00000000000..c98c49112af --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookKernelAssociation.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { IConfigurationNode, IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; +import { Registry } from 'vs/platform/registry/common/platform'; + +export class NotebookKernelProviderAssociationRegistry { + static extensionIds: (string | null)[] = []; + static extensionDescriptions: string[] = []; +} + +export class NotebookViewTypesExtensionRegistry { + static viewTypes: string[] = []; + static viewTypeDescriptions: string[] = []; +} + +export type NotebookKernelProviderAssociation = { + readonly viewType: string; + readonly kernelProvider?: string; +}; + +export type NotebookKernelProviderAssociations = readonly NotebookKernelProviderAssociation[]; + + +export const notebookKernelProviderAssociationsSettingId = 'notebook.kernelProviderAssociations'; + +export const viewTypeSchamaAddition: IJSONSchema = { + type: 'string', + enum: [] +}; + +export const notebookKernelProviderAssociationsConfigurationNode: IConfigurationNode = { + ...workbenchConfigurationNodeBase, + properties: { + [notebookKernelProviderAssociationsSettingId]: { + type: 'array', + markdownDescription: nls.localize('notebook.kernelProviderAssociations', "Defines a default kernel provider which takes precedence over all other kernel providers settings. Must be the identifier of an extension contributing a kernel provider."), + items: { + type: 'object', + defaultSnippets: [{ + body: { + 'viewType': '$1', + 'kernelProvider': '$2' + } + }], + properties: { + 'viewType': { + type: ['string', 'null'], + default: null, + enum: NotebookViewTypesExtensionRegistry.viewTypes, + markdownEnumDescriptions: NotebookViewTypesExtensionRegistry.viewTypeDescriptions + }, + 'kernelProvider': { + type: ['string', 'null'], + default: null, + enum: NotebookKernelProviderAssociationRegistry.extensionIds, + markdownEnumDescriptions: NotebookKernelProviderAssociationRegistry.extensionDescriptions + } + } + } + } + } +}; + +export function updateNotebookKernelProvideAssociationSchema(): void { + Registry.as(Extensions.Configuration) + .notifyConfigurationSchemaUpdated(notebookKernelProviderAssociationsConfigurationNode); +} + +Registry.as(Extensions.Configuration) + .registerConfiguration(notebookKernelProviderAssociationsConfigurationNode); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookPureOutputRenderer.ts b/src/vs/workbench/contrib/notebook/browser/notebookPureOutputRenderer.ts new file mode 100644 index 00000000000..c1f42f14d6c --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookPureOutputRenderer.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { 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 c4db8ed24b9..24821e8fbe3 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -10,7 +10,7 @@ import { notebookProviderExtensionPoint, notebookRendererExtensionPoint, INotebo 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, NotebookDocumentMetadata, ICellDto2, 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 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +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'; @@ -21,7 +21,7 @@ import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/mode 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 { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +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'; @@ -29,11 +29,56 @@ import { StorageScope, IStorageService } from 'vs/platform/storage/common/storag 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 { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions'; +import { CopyAction, CutAction, PasteAction } from 'vs/editor/contrib/clipboard/clipboard'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; function MODEL_ID(resource: URI): string { return resource.toString(); } +export class NotebookKernelProviderInfoStore extends Disposable { + private readonly _notebookKernelProviders: INotebookKernelProvider[] = []; + + constructor() { + super(); + } + + add(provider: INotebookKernelProvider) { + this._notebookKernelProviders.push(provider); + this._updateProviderExtensionsInfo(); + + return toDisposable(() => { + const idx = this._notebookKernelProviders.indexOf(provider); + if (idx >= 0) { + this._notebookKernelProviders.splice(idx, 1); + } + + this._updateProviderExtensionsInfo(); + }); + } + + get(viewType: string, resource: URI) { + return this._notebookKernelProviders.filter(provider => notebookDocumentFilterMatch(provider.selector, viewType, resource)); + } + + private _updateProviderExtensionsInfo() { + NotebookKernelProviderAssociationRegistry.extensionIds.length = 0; + NotebookKernelProviderAssociationRegistry.extensionDescriptions.length = 0; + + this._notebookKernelProviders.forEach(provider => { + NotebookKernelProviderAssociationRegistry.extensionIds.push(provider.providerExtensionId); + NotebookKernelProviderAssociationRegistry.extensionDescriptions.push(provider.providerDescription || ''); + }); + + updateNotebookKernelProvideAssociationSchema(); + } +} + export class NotebookProviderInfoStore extends Disposable { private static readonly CUSTOM_EDITORS_STORAGE_ID = 'notebookEditors'; private static readonly CUSTOM_EDITORS_ENTRY_ID = 'editors'; @@ -53,6 +98,8 @@ export class NotebookProviderInfoStore extends Disposable { this.add(new NotebookProviderInfo(info)); } + this._updateProviderExtensionsInfo(); + this._register(extensionService.onDidRegisterExtensions(() => { if (!this._handled) { // there is no extension point registered for notebook content provider @@ -60,6 +107,8 @@ export class NotebookProviderInfoStore extends Disposable { this.clear(); mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = []; this._memento.saveMemento(); + + this._updateProviderExtensionsInfo(); } })); } @@ -75,7 +124,8 @@ export class NotebookProviderInfoStore extends Disposable { displayName: notebookContribution.displayName, selector: notebookContribution.selector || [], priority: this._convertPriority(notebookContribution.priority), - providerId: extension.description.identifier.value, + providerExtensionId: extension.description.identifier.value, + providerDescription: extension.description.description, providerDisplayName: extension.description.isBuiltin ? nls.localize('builtinProviderDisplayName', "Built-in") : extension.description.displayName || extension.description.identifier.value, providerExtensionLocation: extension.description.extensionLocation })); @@ -85,6 +135,22 @@ export class NotebookProviderInfoStore extends Disposable { const mementoObject = this._memento.getMemento(StorageScope.GLOBAL); mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._contributedEditors.values()); this._memento.saveMemento(); + + this._updateProviderExtensionsInfo(); + } + + private _updateProviderExtensionsInfo() { + NotebookViewTypesExtensionRegistry.viewTypes.length = 0; + NotebookViewTypesExtensionRegistry.viewTypeDescriptions.length = 0; + + for (const contribute of this._contributedEditors) { + if (contribute[1].providerExtensionId) { + NotebookViewTypesExtensionRegistry.viewTypes.push(contribute[1].id); + NotebookViewTypesExtensionRegistry.viewTypeDescriptions.push(`${contribute[1].displayName}`); + } + } + + updateNotebookKernelProvideAssociationSchema(); } private _convertPriority(priority?: string) { @@ -166,14 +232,17 @@ class ModelData implements IDisposable { } export class NotebookService extends Disposable implements INotebookService, ICustomEditorViewTypesHandler { 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 _onDidChangeActiveEditor = new Emitter(); onDidChangeActiveEditor: Event = this._onDidChangeActiveEditor.event; + private _activeEditorDisposables = new DisposableStore(); private _onDidChangeVisibleEditors = new Emitter(); onDidChangeVisibleEditors: Event = this._onDidChangeVisibleEditors.event; private readonly _onNotebookEditorAdd: Emitter = this._register(new Emitter()); @@ -184,6 +253,8 @@ export class NotebookService extends Disposable implements INotebookService, ICu public readonly onNotebookDocumentAdd: Event = this._onNotebookDocumentAdd.event; private readonly _onNotebookDocumentRemove: Emitter = this._register(new Emitter()); public readonly onNotebookDocumentRemove: Event = this._onNotebookDocumentRemove.event; + private readonly _onNotebookDocumentSaved: Emitter = this._register(new Emitter()); + public readonly onNotebookDocumentSaved: Event = this._onNotebookDocumentSaved.event; private readonly _notebookEditors = new Map(); private readonly _onDidChangeViewTypes = new Emitter(); @@ -191,18 +262,20 @@ export class NotebookService extends Disposable implements INotebookService, ICu 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; private _lastClipboardIsCopy: boolean = true; private _displayOrder: { userOrder: string[], defaultOrder: string[] } = Object.create(null); - private readonly _notebookKernelProviders: INotebookKernelProvider[] = []; constructor( @IExtensionService private readonly _extensionService: IExtensionService, @IEditorService private readonly _editorService: IEditorService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, - @IStorageService private readonly _storageService: IStorageService + @IStorageService private readonly _storageService: IStorageService, + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); @@ -221,8 +294,12 @@ export class NotebookService extends Disposable implements INotebookService, ICu this.notebookRenderersInfoStore.add(new NotebookOutputRendererInfo({ id: notebookContribution.viewType, displayName: notebookContribution.displayName, - mimeTypes: notebookContribution.mimeTypes || [] + mimeTypes: notebookContribution.mimeTypes || [], })); + + if (notebookContribution.entrypoint) { + this._notebookRenderers.set(notebookContribution.viewType, new PureNotebookOutputRenderer(notebookContribution.viewType, extension.description, notebookContribution.entrypoint)); + } } } @@ -232,7 +309,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu this._editorService.registerCustomEditorViewTypesHandler('Notebook', this); const updateOrder = () => { - let userOrder = this._configurationService.getValue('notebook.displayOrder'); + const userOrder = this._configurationService.getValue('notebook.displayOrder'); this._displayOrder = { defaultOrder: this._accessibilityService.isScreenReaderOptimized() ? ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER : NOTEBOOK_DISPLAY_ORDER, userOrder: userOrder @@ -250,6 +327,139 @@ export class NotebookService extends Disposable implements INotebookService, ICu this._register(this._accessibilityService.onDidChangeScreenReaderOptimized(() => { updateOrder(); })); + + const getContext = () => { + const editor = getActiveNotebookEditor(this._editorService); + const activeCell = editor?.getActiveCell(); + + return { + editor, + activeCell + }; + }; + + const PRIORITY = 50; + this._register(UndoCommand.addImplementation(PRIORITY, () => { + const { editor } = getContext(); + if (editor?.viewModel) { + editor?.viewModel.undo(); + return true; + } + + return false; + })); + + this._register(RedoCommand.addImplementation(PRIORITY, () => { + const { editor } = getContext(); + if (editor?.viewModel) { + editor?.viewModel.redo(); + return true; + } + + return false; + })); + + if (CopyAction) { + this._register(CopyAction.addImplementation(PRIORITY, accessor => { + const { editor, activeCell } = getContext(); + if (!editor || !activeCell) { + return false; + } + + if (editor.hasOutputTextSelection()) { + document.execCommand('copy'); + return true; + } + + const clipboardService = accessor.get(IClipboardService); + const notebookService = accessor.get(INotebookService); + clipboardService.writeText(activeCell.getText()); + notebookService.setToCopy([activeCell.model], true); + + return true; + })); + } + + if (PasteAction) { + PasteAction.addImplementation(PRIORITY, () => { + const pasteCells = this.getToCopy(); + + if (!pasteCells) { + return false; + } + + const { editor, activeCell } = getContext(); + if (!editor || !activeCell) { + return false; + } + + const viewModel = editor.viewModel; + + if (!viewModel) { + return false; + } + + if (!viewModel.metadata.editable) { + return false; + } + + const currCellIndex = viewModel.getCellIndex(activeCell); + + 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 => { + const newIdx = typeof currCellIndex === 'number' ? currCellIndex + 1 : 0; + topPastedCell = viewModel.insertCell(newIdx, pasteCell, true); + }); + + if (topPastedCell) { + editor.focusNotebookCell(topPastedCell, 'container'); + } + + return true; + }); + } + + if (CutAction) { + CutAction.addImplementation(PRIORITY, accessor => { + const { editor, activeCell } = getContext(); + if (!editor || !activeCell) { + return false; + } + + const viewModel = editor.viewModel; + + if (!viewModel) { + return false; + } + + if (!viewModel.metadata.editable) { + return false; + } + + const clipboardService = accessor.get(IClipboardService); + const notebookService = accessor.get(INotebookService); + clipboardService.writeText(activeCell.getText()); + viewModel.deleteCell(viewModel.getCellIndex(activeCell), true); + notebookService.setToCopy([activeCell.model], false); + + return true; + }); + } + } getViewTypes(): ICustomEditorInfo[] { @@ -301,21 +511,20 @@ export class NotebookService extends Disposable implements INotebookService, ICu } registerNotebookKernelProvider(provider: INotebookKernelProvider): IDisposable { - this._notebookKernelProviders.push(provider); + const d = this.notebookKernelProviderInfoStore.add(provider); const kernelChangeEventListener = provider.onDidChangeKernels(() => { this._onDidChangeKernels.fire(); }); + + this._onDidChangeKernels.fire(); return toDisposable(() => { kernelChangeEventListener.dispose(); - let idx = this._notebookKernelProviders.indexOf(provider); - if (idx >= 0) { - this._notebookKernelProviders.splice(idx, 1); - } + d.dispose(); }); } async getContributedNotebookKernels2(viewType: string, resource: URI, token: CancellationToken): Promise { - const filteredProvider = this._notebookKernelProviders.filter(provider => notebookDocumentFilterMatch(provider.selector, viewType, resource)); + const filteredProvider = this.notebookKernelProviderInfoStore.get(viewType, resource); const result = new Array(filteredProvider.length); const promises = filteredProvider.map(async (provider, index) => { @@ -323,17 +532,21 @@ export class NotebookService extends Disposable implements INotebookService, ICu result[index] = data.map(dto => { return { extension: dto.extension, - extensionLocation: dto.extensionLocation, + extensionLocation: URI.revive(dto.extensionLocation), id: dto.id, label: dto.label, description: dto.description, isPreferred: dto.isPreferred, preloads: dto.preloads, + providerHandle: dto.providerHandle, resolve: async (uri: URI, editorId: string, token: CancellationToken) => { return provider.resolveKernel(editorId, uri, dto.id, token); }, - executeNotebookCell: async (uri: URI, handle: number | undefined, token: CancellationToken) => { - return provider.executeNotebook(uri, dto.id, handle, token); + executeNotebookCell: async (uri: URI, handle: number | undefined) => { + return provider.executeNotebook(uri, dto.id, handle); + }, + cancelNotebookCell: (uri: URI, handle: number | undefined): Promise => { + return provider.cancelNotebook(uri, dto.id, handle); } }; }); @@ -390,43 +603,33 @@ export class NotebookService extends Disposable implements INotebookService, ICu return renderer; } - async createNotebookFromBackup(viewType: string, uri: URI, metadata: NotebookDocumentMetadata, languages: string[], cells: ICellDto2[], editorId?: string): Promise { - const provider = this._notebookProviders.get(viewType); - if (!provider) { - return undefined; - } - - const notebookModel = await provider.controller.createNotebook(viewType, uri, { metadata, languages, cells }, false, editorId); - if (!notebookModel) { - return undefined; - } - - // new notebook model created - const modelId = MODEL_ID(uri); - const modelData = new ModelData( - notebookModel, - (model) => this._onWillDisposeDocument(model), - ); - this._models.set(modelId, 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!); - return modelData.model; - } - async resolveNotebook(viewType: string, uri: URI, forceReload: boolean, editorId?: string, backupId?: string): Promise { const provider = this._notebookProviders.get(viewType); if (!provider) { return undefined; } - const notebookModel = await provider.controller.createNotebook(viewType, uri, undefined, forceReload, editorId, backupId); - if (!notebookModel) { - return undefined; + const modelId = MODEL_ID(uri); + + let notebookModel: NotebookTextModel | undefined = undefined; + if (this._models.has(modelId)) { + // the model already exists + notebookModel = this._models.get(modelId)!.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 modelId = MODEL_ID(uri); const modelData = new ModelData( notebookModel!, (model) => this._onWillDisposeDocument(model), @@ -444,13 +647,19 @@ export class NotebookService extends Disposable implements INotebookService, ICu return modelData.model; } + getNotebookTextModel(uri: URI): NotebookTextModel | undefined { + const modelId = MODEL_ID(uri); + + return this._models.get(modelId)?.model; + } + private async _fillInTransformedOutputs( renderers: Set, requestItems: IOutputRenderRequestCellInfo[], renderFunc: (rendererId: string, items: IOutputRenderRequestCellInfo[]) => Promise | undefined>, lookUp: (key: T) => { outputs: IProcessedOutput[] } ) { - for (let id of renderers) { + for (const id of renderers) { const requestsPerRenderer: IOutputRenderRequestCellInfo[] = requestItems.map(req => { return { key: req.key, @@ -622,14 +831,14 @@ export class NotebookService extends Disposable implements INotebookService, ICu } private _transformMimeTypes(output: IDisplayOutput, outputId: string, documentDisplayOrder: string[]): ITransformedDisplayOutputDto { - let mimeTypes = Object.keys(output.data); - let coreDisplayOrder = this._displayOrder; + const mimeTypes = Object.keys(output.data); + const coreDisplayOrder = this._displayOrder; const sorted = sortMimeTypes(mimeTypes, coreDisplayOrder?.userOrder || [], documentDisplayOrder, coreDisplayOrder?.defaultOrder || []); - let orderMimeTypes: IOrderedMimeType[] = []; + const orderMimeTypes: IOrderedMimeType[] = []; sorted.forEach(mimeType => { - let handlers = this.findBestMatchedRenderer(mimeType); + const handlers = this._findBestMatchedRenderer(mimeType); if (handlers.length) { const handler = handlers[0]; @@ -673,38 +882,55 @@ export class NotebookService extends Disposable implements INotebookService, ICu }; } - findBestMatchedRenderer(mimeType: string): readonly NotebookOutputRendererInfo[] { + private _findBestMatchedRenderer(mimeType: string): readonly NotebookOutputRendererInfo[] { return this.notebookRenderersInfoStore.getContributedRenderer(mimeType); } - async executeNotebook(viewType: string, uri: URI, token: CancellationToken): Promise { - let provider = this._notebookProviders.get(viewType); + async executeNotebook(viewType: string, uri: URI): Promise { + const provider = this._notebookProviders.get(viewType); if (provider) { - return provider.controller.executeNotebookByAttachedKernel(viewType, uri, token); + return provider.controller.executeNotebookByAttachedKernel(viewType, uri); } return; } - async executeNotebookCell(viewType: string, uri: URI, handle: number, token: CancellationToken): Promise { + async executeNotebookCell(viewType: string, uri: URI, handle: number): Promise { const provider = this._notebookProviders.get(viewType); if (provider) { - await provider.controller.executeNotebookCell(uri, handle, token); + await provider.controller.executeNotebookCell(uri, handle); } } - async executeNotebook2(viewType: string, uri: URI, kernelId: string, token: CancellationToken): Promise { - const kernel = this._notebookKernels.get(kernelId); - if (kernel) { - await kernel.executeNotebook(viewType, uri, undefined, token); + 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 executeNotebookCell2(viewType: string, uri: URI, handle: number, kernelId: string, token: CancellationToken): Promise { + async executeNotebook2(viewType: string, uri: URI, kernelId: string): Promise { const kernel = this._notebookKernels.get(kernelId); if (kernel) { - await kernel.executeNotebook(viewType, uri, handle, token); + 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); } } @@ -721,7 +947,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu } getNotebookProviderResourceRoots(): URI[] { - let ret: URI[] = []; + const ret: URI[] = []; this._notebookProviders.forEach(val => { ret.push(URI.revive(val.extensionData.location)); }); @@ -730,7 +956,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu } removeNotebookEditor(editor: INotebookEditor) { - let editorCache = this._notebookEditors.get(editor.getId()); + const editorCache = this._notebookEditors.get(editor.getId()); if (editorCache) { this._notebookEditors.delete(editor.getId()); @@ -768,6 +994,17 @@ export class NotebookService extends Disposable implements INotebookService, ICu } updateActiveNotebookEditor(editor: INotebookEditor | null) { + this._activeEditorDisposables.clear(); + + if (editor) { + this._activeEditorDisposables.add(editor.onDidChangeKernel(() => { + this._onDidChangeNotebookActiveKernel.fire({ + uri: editor.uri!, + providerHandle: editor.activeKernel?.providerHandle, + kernelId: editor.activeKernel?.id + }); + })); + } this._onDidChangeActiveEditor.fire(editor ? editor.getId() : null); } @@ -790,27 +1027,37 @@ export class NotebookService extends Disposable implements INotebookService, ICu } async save(viewType: string, resource: URI, token: CancellationToken): Promise { - let provider = this._notebookProviders.get(viewType); + const provider = this._notebookProviders.get(viewType); if (provider) { - return provider.controller.save(resource, token); + const ret = await provider.controller.save(resource, token); + if (ret) { + this._onNotebookDocumentSaved.fire(resource); + } + + return ret; } return false; } async saveAs(viewType: string, resource: URI, target: URI, token: CancellationToken): Promise { - let provider = this._notebookProviders.get(viewType); + const provider = this._notebookProviders.get(viewType); if (provider) { - return provider.controller.saveAs(resource, target, token); + const ret = await provider.controller.saveAs(resource, target, token); + if (ret) { + this._onNotebookDocumentSaved.fire(resource); + } + + return ret; } return false; } async backup(viewType: string, uri: URI, token: CancellationToken): Promise { - let provider = this._notebookProviders.get(viewType); + const provider = this._notebookProviders.get(viewType); if (provider) { return provider.controller.backup(uri, token); @@ -820,7 +1067,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu } onDidReceiveMessage(viewType: string, editorId: string, rendererType: string | undefined, message: any): void { - let provider = this._notebookProviders.get(viewType); + const provider = this._notebookProviders.get(viewType); if (provider) { return provider.controller.onDidReceiveMessage(editorId, rendererType, message); @@ -828,9 +1075,9 @@ export class NotebookService extends Disposable implements INotebookService, ICu } private _onWillDisposeDocument(model: INotebookTextModel): void { - let modelId = MODEL_ID(model.uri); + const modelId = MODEL_ID(model.uri); - let modelData = this._models.get(modelId); + const modelData = this._models.get(modelId); this._models.delete(modelId); if (modelData) { @@ -844,10 +1091,11 @@ export class NotebookService extends Disposable implements INotebookService, ICu willRemovedEditors.forEach(e => this._notebookEditors.delete(e.getId())); - let provider = this._notebookProviders.get(modelData!.model.viewType); + const provider = this._notebookProviders.get(modelData!.model.viewType); if (provider) { - provider.controller.removeNotebookDocument(modelData!.model); + provider.controller.removeNotebookDocument(modelData!.model.uri); + modelData!.model.dispose(); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index ffd7d4f2e1b..687a666c773 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -19,7 +19,7 @@ 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 } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellRevealPosition, CellRevealType, CursorAtBoundary, getVisibleCells, ICellRange, ICellViewModel, INotebookCellList, reduceCellRanges, CellEditState, CellFocusMode, BaseCellRenderTemplate, NOTEBOOK_CELL_LIST_FOCUSED } 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 { clamp } from 'vs/base/common/numbers'; @@ -75,6 +75,7 @@ export class NotebookCellList extends WorkbenchList implements ID @IKeybindingService keybindingService: IKeybindingService ) { super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService); + NOTEBOOK_CELL_LIST_FOCUSED.bindTo(this.contextKeyService).set(true); this._focusNextPreviousDelegate = options.focusNextPreviousDelegate; this._previousFocusedElements = this.getFocusedElements(); this._localDisposableStore.add(this.onDidChangeFocus((e) => { @@ -144,7 +145,8 @@ export class NotebookCellList extends WorkbenchList implements ID this._localDisposableStore.add(this.view.onMouseDblClick(() => { const focus = this.getFocusedElements()[0]; - if (focus && focus.cellKind === CellKind.Markdown) { + + if (focus && focus.cellKind === CellKind.Markdown && !focus.metadata?.inputCollapsed) { focus.editState = CellEditState.Editing; focus.focusMode = CellFocusMode.Editor; } @@ -162,7 +164,7 @@ export class NotebookCellList extends WorkbenchList implements ID } elementHeight(element: ICellViewModel): number { - let index = this._getViewIndexUpperBound(element); + const index = this._getViewIndexUpperBound(element); if (index === undefined || index < 0 || index >= this.length) { this._getViewIndexUpperBound(element); throw new ListError(this.listUser, `Invalid index ${index}`); @@ -299,7 +301,7 @@ export class NotebookCellList extends WorkbenchList implements ID // set hidden ranges prefix sum let start = 0; let index = 0; - let ret: number[] = []; + const ret: number[] = []; while (index < newRanges.length) { for (let j = start; j < newRanges[index].start - 1; j++) { @@ -540,7 +542,7 @@ export class NotebookCellList extends WorkbenchList implements ID } getAbsoluteTopOfElement(element: ICellViewModel): number { - let index = this._getViewIndexUpperBound(element); + const index = this._getViewIndexUpperBound(element); if (index === undefined || index < 0 || index >= this.length) { this._getViewIndexUpperBound(element); throw new ListError(this.listUser, `Invalid index ${index}`); @@ -666,8 +668,8 @@ export class NotebookCellList extends WorkbenchList implements ID private async _revealRangeInCenterInternalAsync(viewIndex: number, range: Range, revealType: CellRevealType): Promise { const reveal = (viewIndex: number, range: Range, revealType: CellRevealType) => { const element = this.view.element(viewIndex); - let positionOffset = element.getPositionScrollTopOffset(range.startLineNumber, range.startColumn); - let positionOffsetInView = this.view.elementTop(viewIndex) + positionOffset; + const positionOffset = element.getPositionScrollTopOffset(range.startLineNumber, range.startColumn); + const positionOffsetInView = this.view.elementTop(viewIndex) + positionOffset; this.view.setScrollTop(positionOffsetInView - this.view.renderHeight / 2); if (revealType === CellRevealType.Range) { @@ -698,8 +700,8 @@ export class NotebookCellList extends WorkbenchList implements ID private async _revealRangeInCenterIfOutsideViewportInternalAsync(viewIndex: number, range: Range, revealType: CellRevealType): Promise { const reveal = (viewIndex: number, range: Range, revealType: CellRevealType) => { const element = this.view.element(viewIndex); - let positionOffset = element.getPositionScrollTopOffset(range.startLineNumber, range.startColumn); - let positionOffsetInView = this.view.elementTop(viewIndex) + positionOffset; + const positionOffset = element.getPositionScrollTopOffset(range.startLineNumber, range.startColumn); + const positionOffsetInView = this.view.elementTop(viewIndex) + positionOffset; this.view.setScrollTop(positionOffsetInView - this.view.renderHeight / 2); if (revealType === CellRevealType.Range) { 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 cbc254803dd..7e988c86142 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts @@ -20,7 +20,7 @@ export class OutputRenderer { this._contributions = {}; this._mimeTypeMapping = {}; - let contributions = NotebookRegistry.getOutputTransformContributions(); + const contributions = NotebookRegistry.getOutputTransformContributions(); for (const desc of contributions) { try { @@ -44,7 +44,7 @@ export class OutputRenderer { } render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput { - let transform = this._mimeTypeMapping[output.outputKind]; + const transform = this._mimeTypeMapping[output.outputKind]; if (transform) { return transform.render(output, container, preferredMimeType); 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 fba5fe9f3e6..7998129daa4 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 @@ -174,7 +174,7 @@ export function handleANSIOutput(text: string, themeService: IThemeService): HTM * @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code } */ function setBasicFormatters(styleCodes: number[]): void { - for (let code of styleCodes) { + for (const code of styleCodes) { switch (code) { case 0: { styleNames = []; 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 423a2c5ec4c..73a1fd4af7e 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 @@ -54,12 +54,12 @@ class RichRenderer implements IOutputTransformContribution { if (!preferredMimeType || !this._richMimeTypeRenderers.has(preferredMimeType)) { const contentNode = document.createElement('p'); - let mimeTypes = []; + const mimeTypes = []; for (const property in output.data) { mimeTypes.push(property); } - let mimeTypesMessage = mimeTypes.join(', '); + const mimeTypesMessage = mimeTypes.join(', '); if (preferredMimeType) { contentNode.innerText = `No renderer could be found for MIME type: ${preferredMimeType}`; @@ -74,13 +74,13 @@ class RichRenderer implements IOutputTransformContribution { }; } - let renderer = this._richMimeTypeRenderers.get(preferredMimeType); + const renderer = this._richMimeTypeRenderers.get(preferredMimeType); return renderer!(output, container); } renderJSON(output: ITransformedDisplayOutputDto, container: HTMLElement) { - let data = output.data['application/json']; - let str = JSON.stringify(data, null, '\t'); + const data = output.data['application/json']; + const str = JSON.stringify(data, null, '\t'); const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { ...getOutputSimpleEditorOptions(), @@ -92,14 +92,14 @@ class RichRenderer implements IOutputTransformContribution { isSimpleWidget: true }); - let mode = this.modeService.create('json'); - let resource = URI.parse(`notebook-output-${Date.now()}.json`); + const mode = this.modeService.create('json'); + const resource = URI.parse(`notebook-output-${Date.now()}.json`); const textModel = this.modelService.createModel(str, mode, resource, false); editor.setModel(textModel); - let width = this.notebookEditor.getLayoutInfo().width; - let fontInfo = this.notebookEditor.getLayoutInfo().fontInfo; - let height = Math.min(textModel.getLineCount(), 16) * (fontInfo.lineHeight || 18); + const width = this.notebookEditor.getLayoutInfo().width; + const fontInfo = this.notebookEditor.getLayoutInfo().fontInfo; + const height = Math.min(textModel.getLineCount(), 16) * (fontInfo.lineHeight || 18); editor.layout({ height, @@ -114,8 +114,8 @@ class RichRenderer implements IOutputTransformContribution { } renderCode(output: ITransformedDisplayOutputDto, container: HTMLElement) { - let data = output.data['text/x-javascript']; - let str = (isArray(data) ? data.join('') : data) as string; + const data = output.data['text/x-javascript']; + const str = (isArray(data) ? data.join('') : data) as string; const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { ...getOutputSimpleEditorOptions(), @@ -127,14 +127,14 @@ class RichRenderer implements IOutputTransformContribution { isSimpleWidget: true }); - let mode = this.modeService.create('javascript'); - let resource = URI.parse(`notebook-output-${Date.now()}.js`); + const mode = this.modeService.create('javascript'); + const resource = URI.parse(`notebook-output-${Date.now()}.js`); const textModel = this.modelService.createModel(str, mode, resource, false); editor.setModel(textModel); - let width = this.notebookEditor.getLayoutInfo().width; - let fontInfo = this.notebookEditor.getLayoutInfo().fontInfo; - let height = Math.min(textModel.getLineCount(), 16) * (fontInfo.lineHeight || 18); + const width = this.notebookEditor.getLayoutInfo().width; + const fontInfo = this.notebookEditor.getLayoutInfo().fontInfo; + const height = Math.min(textModel.getLineCount(), 16) * (fontInfo.lineHeight || 18); editor.layout({ height, @@ -149,9 +149,9 @@ class RichRenderer implements IOutputTransformContribution { } renderJavaScript(output: ITransformedDisplayOutputDto, container: HTMLElement) { - let data = output.data['application/javascript']; - let str = isArray(data) ? data.join('') : data; - let scriptVal = ``; + const data = output.data['application/javascript']; + const str = isArray(data) ? data.join('') : data; + const scriptVal = ``; return { shadowContent: scriptVal, hasDynamicHeight: false @@ -159,8 +159,8 @@ class RichRenderer implements IOutputTransformContribution { } renderHTML(output: ITransformedDisplayOutputDto, container: HTMLElement) { - let data = output.data['text/html']; - let str = (isArray(data) ? data.join('') : data) as string; + const data = output.data['text/html']; + const str = (isArray(data) ? data.join('') : data) as string; return { shadowContent: str, hasDynamicHeight: false @@ -169,8 +169,8 @@ class RichRenderer implements IOutputTransformContribution { } renderSVG(output: ITransformedDisplayOutputDto, container: HTMLElement) { - let data = output.data['image/svg+xml']; - let str = (isArray(data) ? data.join('') : data) as string; + const data = output.data['image/svg+xml']; + const str = (isArray(data) ? data.join('') : data) as string; return { shadowContent: str, hasDynamicHeight: false @@ -178,7 +178,7 @@ class RichRenderer implements IOutputTransformContribution { } renderMarkdown(output: ITransformedDisplayOutputDto, container: HTMLElement) { - let data = output.data['text/markdown']; + 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); @@ -215,8 +215,8 @@ class RichRenderer implements IOutputTransformContribution { } renderPlainText(output: ITransformedDisplayOutputDto, container: HTMLElement) { - let data = output.data['text/plain']; - let str = (isArray(data) ? data.join('') : data) as string; + 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); 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 41761f5ce9e..7f37bd88b20 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -107,6 +107,7 @@ export interface IContentWidgetTopRequest { export interface IViewScrollTopRequestMessage { type: 'view-scroll'; top?: number; + forceDisplay: boolean; widgets: IContentWidgetTopRequest[]; version: number; } @@ -219,7 +220,7 @@ export interface INotebookWebviewMessage { let version = 0; export class BackLayerWebView extends Disposable { element: HTMLElement; - webview!: WebviewElement; + webview: WebviewElement | undefined = undefined; insetMapping: Map = new Map(); hiddenInsetMapping: Set = new Set(); reversedInsetMapping: Map = new Map(); @@ -273,6 +274,10 @@ export class BackLayerWebView extends Disposable { background-color: var(--vscode-notebook-symbolHighlightBackground); } + #container > div > div > div { + overflow-x: scroll; + } + body { padding: 0px; height: 100%; @@ -340,6 +345,13 @@ export class BackLayerWebView extends Disposable { return; } + const cell = this.insetMapping.get(output)!.cell; + + const currCell = this.notebookEditor.viewModel?.viewCells.find(vc => vc.handle === cell.handle); + if (currCell !== cell && currCell !== undefined) { + this.insetMapping.get(output)!.cell = currCell as CodeCellViewModel; + } + return { cell: this.insetMapping.get(output)!.cell, output }; } @@ -427,13 +439,13 @@ ${loaderJs} if (data.__vscode_notebook_message) { if (data.type === 'dimension') { - let height = data.data.height; - let outputHeight = height; + const height = data.data.height; + const outputHeight = height; const info = this.resolveOutputId(data.id); if (info) { const { cell, output } = info; - let outputIndex = cell.outputs.indexOf(output); + const outputIndex = cell.outputs.indexOf(output); cell.updateOutputHeight(outputIndex, outputHeight); this.notebookEditor.layoutNotebookCell(cell, cell.layoutInfo.totalHeight); } @@ -545,7 +557,7 @@ ${loaderJs} resolveFunc = resolve; }); - let dispose = webview.onMessage((data: FromWebviewMessage) => { + const dispose = webview.onMessage((data: FromWebviewMessage) => { if (data.__vscode_notebook_message && data.type === 'initialized') { resolveFunc(); dispose.dispose(); @@ -561,9 +573,13 @@ ${loaderJs} return; } - let outputCache = this.insetMapping.get(output)!; - let outputIndex = cell.outputs.indexOf(output); - let outputOffset = cellTop + cell.getOutputOffset(outputIndex); + if (cell.metadata?.outputCollapsed) { + return false; + } + + const outputCache = this.insetMapping.get(output)!; + const outputIndex = cell.outputs.indexOf(output); + const outputOffset = cellTop + cell.getOutputOffset(outputIndex); if (this.hiddenInsetMapping.has(output)) { return true; @@ -576,17 +592,17 @@ ${loaderJs} return true; } - updateViewScrollTop(top: number, items: { cell: CodeCellViewModel, output: IProcessedOutput, cellTop: number }[]) { + updateViewScrollTop(top: number, forceDisplay: boolean, items: { cell: CodeCellViewModel, output: IProcessedOutput, cellTop: number }[]) { if (this._disposed) { return; } - let widgets: IContentWidgetTopRequest[] = items.map(item => { - let outputCache = this.insetMapping.get(item.output)!; - let id = outputCache.outputId; - let outputIndex = item.cell.outputs.indexOf(item.output); + const widgets: IContentWidgetTopRequest[] = items.map(item => { + const outputCache = this.insetMapping.get(item.output)!; + const id = outputCache.outputId; + const outputIndex = item.cell.outputs.indexOf(item.output); - let outputOffset = item.cellTop + item.cell.getOutputOffset(outputIndex); + const outputOffset = item.cellTop + item.cell.getOutputOffset(outputIndex); outputCache.cachedCreation.top = outputOffset; this.hiddenInsetMapping.delete(item.output); @@ -601,6 +617,7 @@ ${loaderJs} top, type: 'view-scroll', version: version++, + forceDisplay, widgets: widgets }); } @@ -611,10 +628,10 @@ ${loaderJs} } const requiredPreloads = await this.updateRendererPreloads(preloads); - let initialTop = cellTop + offset; + const initialTop = cellTop + offset; if (this.insetMapping.has(output)) { - let outputCache = this.insetMapping.get(output); + const outputCache = this.insetMapping.get(output); if (outputCache) { this.hiddenInsetMapping.delete(output); @@ -628,7 +645,7 @@ ${loaderJs} } } - let outputId = output.outputKind === CellOutputKind.Rich ? output.outputId : UUID.generateUuid(); + 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]; @@ -637,7 +654,7 @@ ${loaderJs} } } - let message: ICreationRequestMessage = { + const message: ICreationRequestMessage = { type: 'html', content: shadowContent, cellId: cell.id, @@ -659,12 +676,12 @@ ${loaderJs} return; } - let outputCache = this.insetMapping.get(output); + const outputCache = this.insetMapping.get(output); if (!outputCache) { return; } - let id = outputCache.outputId; + const id = outputCache.outputId; this._sendMessageToWebview({ type: 'clearOutput', @@ -682,7 +699,7 @@ ${loaderJs} return; } - let outputCache = this.insetMapping.get(output); + const outputCache = this.insetMapping.get(output); if (!outputCache) { return; } @@ -714,7 +731,7 @@ ${loaderJs} return; } - this.webview.focus(); + this.webview?.focus(); } focusOutput(cellId: string) { @@ -722,7 +739,7 @@ ${loaderJs} return; } - this.webview.focus(); + this.webview?.focus(); setTimeout(() => { // Need this, or focus decoration is not shown. No clue. this._sendMessageToWebview({ type: 'focus-output', @@ -748,7 +765,7 @@ ${loaderJs} await this._loaded; - let resources: IPreloadResource[] = []; + const resources: IPreloadResource[] = []; preloads = preloads.map(preload => { if (this.environmentService.isExtensionDevelopment && (preload.scheme === 'http' || preload.scheme === 'https')) { return preload; @@ -778,14 +795,14 @@ ${loaderJs} await this._loaded; - let requiredPreloads: IPreloadResource[] = []; - let resources: IPreloadResource[] = []; - let extensionLocations: URI[] = []; + const requiredPreloads: IPreloadResource[] = []; + const resources: IPreloadResource[] = []; + const extensionLocations: URI[] = []; preloads.forEach(preload => { - let rendererInfo = this.notebookService.getRendererInfo(preload); + const rendererInfo = this.notebookService.getRendererInfo(preload); if (rendererInfo) { - let preloadResources = rendererInfo.preloads.map(preloadResource => { + const preloadResources = rendererInfo.preloads.map(preloadResource => { if (this.environmentService.isExtensionDevelopment && (preloadResource.scheme === 'http' || preloadResource.scheme === 'https')) { return preloadResource; } @@ -814,6 +831,10 @@ ${loaderJs} } private _updatePreloads(resources: IPreloadResource[], source: 'renderer' | 'kernel') { + if (!this.webview) { + return; + } + const mixedResourceRoots = [...(this.localResourceRootsCache || []), ...this.rendererRootsCache, ...this.kernelRootsCache]; this.webview.localResourcesRoot = mixedResourceRoots; @@ -830,7 +851,7 @@ ${loaderJs} return; } - this.webview.postMessage(message); + this.webview?.postMessage(message); } clearPreloadsCache() { @@ -839,7 +860,7 @@ ${loaderJs} dispose() { this._disposed = true; - this.webview.dispose(); + this.webview?.dispose(); super.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 new file mode 100644 index 00000000000..c5ccf32bc91 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { Action, IAction, Separator } from 'vs/base/common/actions'; +import { IMenu, IMenuActionOptions, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; + +export class VerticalSeparator extends Action { + static readonly ID = 'vs.actions.verticalSeparator'; + + constructor( + label?: string + ) { + super(VerticalSeparator.ID, label, label ? 'verticalSeparator text' : 'verticalSeparator'); + this.checked = false; + this.enabled = false; + } +} + +export class VerticalSeparatorViewItem extends BaseActionViewItem { + render(container: HTMLElement) { + DOM.addClass(container, 'verticalSeparator'); + // const iconContainer = DOM.append(container, $('.verticalSeparator')); + // DOM.addClasses(iconContainer, 'codicon', 'codicon-chrome-minimize'); + } +} + +export function createAndFillInActionBarActionsWithVerticalSeparators(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, 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); + 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 { + 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; + + if (to.length > 0) { + to.push(new VerticalSeparator()); + } + + to.push(...actions); + } else { + const to = Array.isArray(target) ? target : target.secondary; + + if (to.length > 0) { + to.push(new Separator()); + } + + to.push(...actions); + } + } +} + +function asDisposable(groups: ReadonlyArray<[string, ReadonlyArray]>): IDisposable { + const disposables = new DisposableStore(); + for (const [, actions] of groups) { + for (const action of actions) { + disposables.add(action); + } + } + return disposables; +} 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 401d6e755f7..3c3b9eac7f4 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts @@ -6,21 +6,23 @@ 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 } 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 } 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'; export class CellContextKeyManager extends Disposable { - private cellType: IContextKey; - private viewType: IContextKey; - private cellEditable: IContextKey; - private cellRunnable: IContextKey; - private cellRunState: IContextKey; - private cellHasOutputs: IContextKey; + private cellType!: IContextKey; + private viewType!: IContextKey; + private cellEditable!: IContextKey; + private cellRunnable!: IContextKey; + private cellRunState!: IContextKey; + private cellHasOutputs!: IContextKey; + private cellContentCollapsed!: IContextKey; + private cellOutputCollapsed!: IContextKey; - private markdownEditMode: IContextKey; + private markdownEditMode!: IContextKey; private elementDisposables = new DisposableStore(); @@ -31,15 +33,19 @@ export class CellContextKeyManager extends Disposable { ) { super(); - 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.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); - this.cellHasOutputs = NOTEBOOK_CELL_HAS_OUTPUTS.bindTo(this.contextKeyService); + this.contextKeyService.bufferChangeEvents(() => { + 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.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); + this.cellHasOutputs = NOTEBOOK_CELL_HAS_OUTPUTS.bindTo(this.contextKeyService); + this.cellContentCollapsed = NOTEBOOK_CELL_INPUT_COLLAPSED.bindTo(this.contextKeyService); + this.cellOutputCollapsed = NOTEBOOK_CELL_OUTPUT_COLLAPSED.bindTo(this.contextKeyService); - this.updateForElement(element); + this.updateForElement(element); + }); } public updateForElement(element: BaseCellViewModel) { @@ -50,6 +56,8 @@ export class CellContextKeyManager extends Disposable { this.elementDisposables.add(element.onDidChangeOutputs(() => this.updateForOutputs())); } + this.elementDisposables.add(element.model.onDidChangeMetadata(() => this.updateForCollapseState())); + this.element = element; if (this.element instanceof MarkdownCellViewModel) { this.cellType.set('markdown'); @@ -57,21 +65,30 @@ export class CellContextKeyManager extends Disposable { this.cellType.set('code'); } - this.updateForMetadata(); - this.updateForEditState(); - this.updateForOutputs(); + this.contextKeyService.bufferChangeEvents(() => { + this.updateForMetadata(); + this.updateForEditState(); + this.updateForCollapseState(); + this.updateForOutputs(); - this.viewType.set(this.element.viewType); + this.viewType.set(this.element.viewType); + }); } private onDidChangeState(e: CellViewModelStateChangeEvent) { - if (e.metadataChanged) { - this.updateForMetadata(); - } + this.contextKeyService.bufferChangeEvents(() => { + if (e.metadataChanged) { + this.updateForMetadata(); + } - if (e.editStateChanged) { - this.updateForEditState(); - } + if (e.editStateChanged) { + this.updateForEditState(); + } + + // if (e.collapseStateChanged) { + // this.updateForCollapseState(); + // } + }); } private updateForMetadata() { @@ -91,6 +108,11 @@ export class CellContextKeyManager extends Disposable { } } + private updateForCollapseState() { + this.cellContentCollapsed.set(!!this.element.metadata?.inputCollapsed); + this.cellOutputCollapsed.set(!!this.element.metadata?.outputCollapsed); + } + private updateForOutputs() { if (this.element instanceof CodeCellViewModel) { this.cellHasOutputs.set(this.element.outputs.length > 0); 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 0857d782123..8c3f6e513b5 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts @@ -3,16 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IAction } from 'vs/base/common/actions'; -import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; export class CellMenus { constructor( @IMenuService private readonly menuService: IMenuService, - @IContextMenuService private readonly contextMenuService: IContextMenuService ) { } getCellTitleMenu(contextKeyService: IContextKeyService): IMenu { @@ -26,11 +22,6 @@ export class CellMenus { private getMenu(menuId: MenuId, contextKeyService: IContextKeyService): IMenu { const menu = this.menuService.createMenu(menuId, contextKeyService); - const primary: IAction[] = []; - const secondary: IAction[] = []; - const result = { primary, secondary }; - - createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => /^inline/.test(g)); return menu; } 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 a34745d0d1f..a4bc32bcf2b 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -27,8 +27,9 @@ 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 { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenu, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenu, MenuItemAction, SubmenuItemAction } 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'; @@ -36,17 +37,18 @@ 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, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING, CELL_BOTTOM_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CancelCellAction, ChangeCellLanguageAction, ExecuteCellAction, INotebookCellActionContext, CELL_TITLE_GROUP_ID } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; -import { BaseCellRenderTemplate, CellEditState, CodeCellRenderTemplate, ICellViewModel, INotebookCellList, INotebookEditor, MarkdownCellRenderTemplate, isCodeCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +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 { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys'; import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; import { CodeCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/codeCell'; -import { StatefullMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/markdownCell'; +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, NotebookCellRunState, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys'; +import { CellKind, NotebookCellMetadata, NotebookCellRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { createAndFillInActionBarActionsWithVerticalSeparators, VerticalSeparator, VerticalSeparatorViewItem } from './cellActionView'; const $ = DOM.$; @@ -77,7 +79,7 @@ export class NotebookCellListDelegate implements IListVirtualDelegate { const bottomToolbarOffset = element.layoutInfo.bottomToolbarOffset; container.style.top = `${bottomToolbarOffset}px`; - - templateData.elementDisposables.add(element.onDidChangeLayout(() => { - const bottomToolbarOffset = element.layoutInfo.bottomToolbarOffset; - container.style.top = `${bottomToolbarOffset}px`; - })); - } + })); } - protected createToolbar(container: HTMLElement): ToolBar { + protected createToolbar(container: HTMLElement, elementClass?: string): ToolBar { const toolbar = new ToolBar(container, this.contextMenuService, { + getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), actionViewItemProvider: action => { if (action instanceof MenuItemAction) { - const item = new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); - return item; + return this.instantiationService.createInstance(MenuEntryActionViewItem, action); + } else if (action instanceof SubmenuItemAction) { + return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); + } + + if (action.id === VerticalSeparator.ID) { + return new VerticalSeparatorViewItem(undefined, action); } return undefined; } }); + if (elementClass) { + toolbar.getElement().classList.add(elementClass); + } + return toolbar; } private getCellToolbarActions(menu: IMenu): { primary: IAction[], secondary: IAction[] } { const primary: IAction[] = []; const secondary: IAction[] = []; - const actions = menu.getActions({ shouldForwardArgs: true }); - for (let [id, menuActions] of actions) { - if (id === CELL_TITLE_GROUP_ID) { - primary.push(...menuActions); - } else { - secondary.push(...menuActions); - } - } + const result = { primary, secondary }; - return { primary, secondary }; + createAndFillInActionBarActionsWithVerticalSeparators(menu, { shouldForwardArgs: true }, result, g => /^inline/.test(g)); + + return result; } protected setupCellToolbarActions(templateData: BaseCellRenderTemplate, disposables: DisposableStore): void { const updateActions = () => { const actions = this.getCellToolbarActions(templateData.titleMenu); - const hadFocus = DOM.isAncestor(document.activeElement, templateData.toolbar.getContainer()); + const hadFocus = DOM.isAncestor(document.activeElement, templateData.toolbar.getElement()); templateData.toolbar.setActions(actions.primary, actions.secondary); if (hadFocus) { this.notebookEditor.focus(); @@ -274,26 +276,47 @@ abstract class AbstractCellRenderer { if (actions.primary.length || actions.secondary.length) { templateData.container.classList.add('cell-has-toolbar-actions'); if (isCodeCellRenderTemplate(templateData)) { - templateData.focusIndicator.style.top = `${EDITOR_TOOLBAR_HEIGHT + EDITOR_TOP_MARGIN}px`; - templateData.focusIndicatorRight.style.top = `${EDITOR_TOOLBAR_HEIGHT + EDITOR_TOP_MARGIN}px`; + templateData.focusIndicatorLeft.style.top = `${EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN}px`; + templateData.focusIndicatorRight.style.top = `${EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN}px`; } } else { templateData.container.classList.remove('cell-has-toolbar-actions'); if (isCodeCellRenderTemplate(templateData)) { - templateData.focusIndicator.style.top = `${EDITOR_TOP_MARGIN}px`; - templateData.focusIndicatorRight.style.top = `${EDITOR_TOP_MARGIN}px`; + templateData.focusIndicatorLeft.style.top = `${CELL_TOP_MARGIN}px`; + templateData.focusIndicatorRight.style.top = `${CELL_TOP_MARGIN}px`; } } }; + // #103926 + let dropdownIsVisible = false; + let deferredUpdate: (() => void) | undefined; + updateActions(); disposables.add(templateData.titleMenu.onDidChange(() => { if (this.notebookEditor.isDisposed) { return; } + if (dropdownIsVisible) { + deferredUpdate = () => updateActions(); + return; + } + updateActions(); })); + disposables.add(templateData.toolbar.onDidChangeDropdownVisibility(visible => { + dropdownIsVisible = visible; + + if (deferredUpdate && !visible) { + setTimeout(() => { + if (deferredUpdate) { + deferredUpdate(); + } + }, 0); + deferredUpdate = undefined; + } + })); } protected commonRenderTemplate(templateData: BaseCellRenderTemplate): void { @@ -302,6 +325,8 @@ abstract class AbstractCellRenderer { this.notebookEditor.selectElement(templateData.currentRenderedCell); } }, true)); + + this.addExpandListener(templateData); } protected commonRenderElement(element: ICellViewModel, index: number, templateData: BaseCellRenderTemplate): void { @@ -311,6 +336,36 @@ abstract class AbstractCellRenderer { templateData.container.classList.remove(DRAGGING_CLASS); } } + + protected addExpandListener(templateData: BaseCellRenderTemplate): void { + templateData.disposables.add(domEvent(templateData.expandButton, DOM.EventType.CLICK)(() => { + if (!templateData.currentRenderedCell) { + return; + } + + if (templateData.currentRenderedCell.metadata?.inputCollapsed) { + this.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(templateData.currentRenderedCell.handle, { inputCollapsed: false }); + } else if (templateData.currentRenderedCell.metadata?.outputCollapsed) { + this.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(templateData.currentRenderedCell.handle, { outputCollapsed: false }); + } + })); + } + + protected setupCollapsedPart(container: HTMLElement): { collapsedPart: HTMLElement, expandButton: HTMLElement } { + const collapsedPart = DOM.append(container, $('.cell.cell-collapsed-part')); + collapsedPart.innerHTML = renderCodicons('$(unfold)'); + const expandButton = collapsedPart.querySelector('.codicon') as HTMLElement; + const keybinding = this.keybindingService.lookupKeybinding(EXPAND_CELL_CONTENT_COMMAND_ID); + let title = localize('cellExpandButtonLabel', "Expand"); + if (keybinding) { + title += ` (${keybinding.getLabel()})`; + } + + collapsedPart.title = title; + DOM.hide(collapsedPart); + + return { collapsedPart, expandButton }; + } } export class MarkdownCellRenderer extends AbstractCellRenderer implements IListRenderer { @@ -334,13 +389,13 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR return MarkdownCellRenderer.TEMPLATE_ID; } - renderTemplate(container: HTMLElement): MarkdownCellRenderTemplate { - container.classList.add('markdown-cell-row'); + renderTemplate(rootContainer: HTMLElement): MarkdownCellRenderTemplate { + rootContainer.classList.add('markdown-cell-row'); + 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)); - const focusIndicator = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-left')); - focusIndicator.setAttribute('draggable', 'true'); + const toolbar = disposables.add(this.createToolbar(container, 'cell-title-toolbar')); + const focusIndicatorLeft = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-left')); const codeInnerContent = DOM.append(container, $('.cell.code')); const editorPart = DOM.append(codeInnerContent, $('.cell-editor-part')); @@ -348,23 +403,25 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR editorPart.style.display = 'none'; const innerContent = DOM.append(container, $('.cell.markdown')); - const foldingIndicator = DOM.append(focusIndicator, DOM.$('.notebook-folding-indicator')); + const foldingIndicator = DOM.append(focusIndicatorLeft, DOM.$('.notebook-folding-indicator')); + + const { collapsedPart, expandButton } = this.setupCollapsedPart(container); const bottomCellContainer = DOM.append(container, $('.cell-bottom-toolbar-container')); - DOM.append(bottomCellContainer, $('.separator')); const betweenCellToolbar = disposables.add(this.createBetweenCellToolbar(bottomCellContainer, disposables, contextKeyService)); - DOM.append(bottomCellContainer, $('.separator')); const statusBar = this.instantiationService.createInstance(CellEditorStatusBar, editorPart); const titleMenu = disposables.add(this.cellMenus.getCellTitleMenu(contextKeyService)); const templateData: MarkdownCellRenderTemplate = { + collapsedPart, + expandButton, contextKeyService, container, cellContainer: innerContent, editorPart, editorContainer, - focusIndicator, + focusIndicatorLeft, foldingIndicator, disposables, elementDisposables: new DisposableStore(), @@ -376,8 +433,9 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR titleMenu, toJSON: () => { return {}; } }; - this.dndController.registerDragHandle(templateData, () => this.getDragImage(templateData)); + this.dndController.registerDragHandle(templateData, rootContainer, container, () => this.getDragImage(templateData)); this.commonRenderTemplate(templateData); + return templateData; } @@ -391,7 +449,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR private getMarkdownDragImage(templateData: MarkdownCellRenderTemplate): HTMLElement { const dragImageContainer = DOM.$('.cell-drag-image.monaco-list-row.focused.markdown-cell-row'); - dragImageContainer.innerHTML = templateData.container.innerHTML; + dragImageContainer.innerHTML = templateData.container.outerHTML; // Remove all rendered content nodes after the const markdownContent = dragImageContainer.querySelector('.cell.markdown')!; @@ -414,7 +472,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR templateData.currentEditor = undefined; templateData.editorPart!.style.display = 'none'; templateData.cellContainer.innerHTML = ''; - let renderedHTML = element.getHTML(); + const renderedHTML = element.getHTML(); if (renderedHTML) { templateData.cellContainer.appendChild(renderedHTML); } @@ -439,7 +497,8 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR this.setBetweenCellToolbarContext(templateData, element, toolbarContext); - const markdownCell = this.instantiationService.createInstance(StatefullMarkdownCell, this.notebookEditor, element, templateData, this.editorOptions.value, this.renderedEditors); + const scopedInstaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, templateData.contextKeyService])); + const markdownCell = scopedInstaService.createInstance(StatefulMarkdownCell, this.notebookEditor, element, templateData, this.editorOptions.value, this.renderedEditors); elementDisposables.add(this.editorOptions.onDidChange(newValue => markdownCell.updateEditorOptions(newValue))); elementDisposables.add(markdownCell); @@ -596,9 +655,9 @@ export class CellDragAndDropController extends Disposable { const dropDirection = this.getDropInsertDirection(event); const insertionIndicatorAbsolutePos = dropDirection === 'above' ? event.cellTop : event.cellTop + event.cellHeight; - const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop; + const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop + BOTTOM_CELL_TOOLBAR_HEIGHT / 2; if (insertionIndicatorTop >= 0) { - this.listInsertionIndicator.style.top = `${insertionIndicatorAbsolutePos - this.list.scrollTop}px`; + this.listInsertionIndicator.style.top = `${insertionIndicatorTop}px`; this.setInsertIndicatorVisibility(true); } else { this.setInsertIndicatorVisibility(false); @@ -611,13 +670,30 @@ export class CellDragAndDropController extends Disposable { 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; + 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 @@ -625,9 +701,9 @@ export class CellDragAndDropController extends Disposable { } if (isCopy) { - this.copyCell(draggedCell, event.draggedOverCell, dropDirection); + this.copyCells(draggedCells, event.draggedOverCell, dropDirection); } else { - this.moveCell(draggedCell, event.draggedOverCell, dropDirection); + this.moveCells(draggedCells, event.draggedOverCell, dropDirection); } } @@ -646,9 +722,9 @@ export class CellDragAndDropController extends Disposable { this.setInsertIndicatorVisibility(false); } - registerDragHandle(templateData: BaseCellRenderTemplate, dragImageProvider: DragImageProvider): void { + registerDragHandle(templateData: BaseCellRenderTemplate, cellRoot: HTMLElement, dragHandle: HTMLElement, dragImageProvider: DragImageProvider): void { const container = templateData.container; - const dragHandle = templateData.focusIndicator; + 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 @@ -665,24 +741,52 @@ export class CellDragAndDropController extends Disposable { this.currentDraggedCell.dragging = true; const dragImage = dragImageProvider(); - container.parentElement!.appendChild(dragImage); + cellRoot.parentElement!.appendChild(dragImage); event.dataTransfer.setDragImage(dragImage, 0, 0); - setTimeout(() => container.parentElement!.removeChild(dragImage!), 0); // Comment this out to debug drag image layout + setTimeout(() => cellRoot.parentElement!.removeChild(dragImage!), 0); // Comment this out to debug drag image layout container.classList.add(DRAGGING_CLASS); })); } - private async moveCell(draggedCell: ICellViewModel, ontoCell: ICellViewModel, direction: 'above' | 'below') { - await this.notebookEditor.moveCell(draggedCell, ontoCell, direction); + 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 copyCell(draggedCell: ICellViewModel, ontoCell: ICellViewModel, direction: 'above' | 'below') { - const editState = draggedCell.editState; - const newCell = this.notebookEditor.insertNotebookCell(ontoCell, draggedCell.cellKind, direction, draggedCell.getText()); - if (newCell) { - this.notebookEditor.focusNotebookCell(newCell, editState === CellEditState.Editing ? 'editor' : 'container'); + 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'); } } @@ -778,8 +882,8 @@ class EditorTextRenderer { } private getDefaultColorMap(): string[] { - let colorMap = modes.TokenizationRegistry.getColorMap(); - let result: string[] = ['#000000']; + const colorMap = modes.TokenizationRegistry.getColorMap(); + const result: string[] = ['#000000']; if (colorMap) { for (let i = 1, len = colorMap.length; i < len; i++) { result[i] = Color.Format.CSS.formatHex(colorMap[i]); @@ -863,15 +967,16 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende return CodeCellRenderer.TEMPLATE_ID; } - renderTemplate(container: HTMLElement): CodeCellRenderTemplate { - container.classList.add('code-cell-row'); + renderTemplate(rootContainer: HTMLElement): CodeCellRenderTemplate { + rootContainer.classList.add('code-cell-row'); + const container = DOM.append(rootContainer, DOM.$('.cell-inner-container')); const disposables = new DisposableStore(); const contextKeyService = disposables.add(this.contextKeyServiceProvider(container)); DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-top')); - const toolbar = disposables.add(this.createToolbar(container)); + const toolbar = disposables.add(this.createToolbar(container, 'cell-title-toolbar')); const focusIndicator = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-left')); - focusIndicator.setAttribute('draggable', 'true'); + const dragHandle = DOM.append(container, DOM.$('.cell-drag-handle')); const cellContainer = DOM.append(container, $('.cell.code')); const runButtonContainer = DOM.append(cellContainer, $('.run-button-container')); @@ -892,44 +997,47 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende dimension: { width: 0, height: 0 - } + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() }, {}); disposables.add(this.editorOptions.onDidChange(newValue => editor.updateOptions(newValue))); + const { collapsedPart, expandButton } = this.setupCollapsedPart(container); + const progressBar = new ProgressBar(editorPart); progressBar.hide(); disposables.add(progressBar); const statusBar = this.instantiationService.createInstance(CellEditorStatusBar, editorPart); const timer = new TimerRenderer(statusBar.durationContainer); + const cellRunState = new RunStateRenderer(statusBar.cellRunStatusContainer, runToolbar, this.instantiationService); const outputContainer = DOM.append(container, $('.output')); const focusIndicatorRight = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-right')); - focusIndicatorRight.setAttribute('draggable', 'true'); const focusSinkElement = DOM.append(container, $('.cell-editor-focus-sink')); focusSinkElement.setAttribute('tabindex', '0'); const bottomCellContainer = DOM.append(container, $('.cell-bottom-toolbar-container')); - DOM.append(bottomCellContainer, $('.separator')); - const betweenCellToolbar = this.createBetweenCellToolbar(bottomCellContainer, disposables, contextKeyService); - DOM.append(bottomCellContainer, $('.separator')); - const focusIndicatorBottom = DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-bottom')); + const betweenCellToolbar = this.createBetweenCellToolbar(bottomCellContainer, disposables, contextKeyService); const titleMenu = disposables.add(this.cellMenus.getCellTitleMenu(contextKeyService)); const templateData: CodeCellRenderTemplate = { + editorPart, + collapsedPart, + expandButton, contextKeyService, container, cellContainer, statusBarContainer: statusBar.statusBarContainer, - cellRunStatusContainer: statusBar.cellRunStatusContainer, + cellRunState, cellStatusMessageContainer: statusBar.cellStatusMessageContainer, languageStatusBarItem: statusBar.languageStatusBarItem, progressBar, - focusIndicator, + focusIndicatorLeft: focusIndicator, focusIndicatorRight, focusIndicatorBottom, toolbar, @@ -945,10 +1053,11 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende bottomCellContainer, timer, titleMenu, + dragHandle, toJSON: () => { return {}; } }; - this.dndController.registerDragHandle(templateData, () => new CodeCellDragImageRenderer().getDragImage(templateData, templateData.editor, 'code')); + this.dndController.registerDragHandle(templateData, rootContainer, dragHandle, () => new CodeCellDragImageRenderer().getDragImage(templateData, templateData.editor, 'code')); disposables.add(DOM.addDisposableListener(focusSinkElement, DOM.EventType.FOCUS, () => { if (templateData.currentRenderedCell && (templateData.currentRenderedCell as CodeCellViewModel).outputs.length) { @@ -961,26 +1070,6 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende return templateData; } - private updateForRunState(runState: NotebookCellRunState | undefined, templateData: CodeCellRenderTemplate): void { - if (typeof runState === 'undefined') { - runState = NotebookCellRunState.Idle; - } - - if (runState === NotebookCellRunState.Running) { - templateData.progressBar.infinite().show(500); - - templateData.runToolbar.setActions([ - this.instantiationService.createInstance(CancelCellAction) - ]); - } else { - templateData.progressBar.hide(); - - templateData.runToolbar.setActions([ - this.instantiationService.createInstance(ExecuteCellAction) - ]); - } - } - private updateForOutputs(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void { if (element.outputs.length) { DOM.show(templateData.focusSinkElement); @@ -995,15 +1084,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende this.updateExecutionOrder(metadata, templateData); templateData.cellStatusMessageContainer.textContent = metadata?.statusMessage || ''; - if (metadata.runState === NotebookCellRunState.Success) { - templateData.cellRunStatusContainer.innerHTML = renderCodicons('$(check)'); - } else if (metadata.runState === NotebookCellRunState.Error) { - templateData.cellRunStatusContainer.innerHTML = renderCodicons('$(error)'); - } else if (metadata.runState === NotebookCellRunState.Running) { - templateData.cellRunStatusContainer.innerHTML = renderCodicons('$(sync~spin)'); - } else { - templateData.cellRunStatusContainer.innerHTML = ''; - } + templateData.cellRunState.renderState(element.metadata?.runState); if (metadata.runState === NotebookCellRunState.Running) { if (metadata.runStartTime) { @@ -1021,7 +1102,11 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende this.editorOptions.setGlyphMargin(metadata.breakpointMargin); } - this.updateForRunState(metadata.runState, templateData); + if (metadata.runState === NotebookCellRunState.Running) { + templateData.progressBar.infinite().show(500); + } else { + templateData.progressBar.hide(); + } } private updateExecutionOrder(metadata: NotebookCellMetadata, templateData: CodeCellRenderTemplate): void { @@ -1040,10 +1125,11 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende } private updateForLayout(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void { - templateData.focusIndicator.style.height = `${element.layoutInfo.indicatorHeight}px`; + 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.outputContainer.style.top = `${element.layoutInfo.outputContainerOffset}px`; + templateData.dragHandle.style.height = `${element.layoutInfo.totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT}px`; } renderElement(element: CodeCellViewModel, index: number, templateData: CodeCellRenderTemplate, height: number | undefined): void { @@ -1080,6 +1166,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende this.updateForLayout(element, templateData); })); + templateData.cellRunState.clear(); this.updateForMetadata(element, templateData); this.updateForHover(element, templateData); elementDisposables.add(element.onDidChangeState((e) => { @@ -1169,3 +1256,50 @@ export class TimerRenderer { return `${seconds}.${tenths}s`; } } + +export class RunStateRenderer { + private static readonly MIN_SPINNER_TIME = 200; + + private spinnerTimer: any | undefined; + private pendingNewState: NotebookCellRunState | undefined; + + constructor(private readonly element: HTMLElement, private readonly runToolbar: ToolBar, private readonly instantiationService: IInstantiationService) { + } + + clear() { + if (this.spinnerTimer) { + clearTimeout(this.spinnerTimer); + } + } + + renderState(runState: NotebookCellRunState = NotebookCellRunState.Idle) { + if (this.spinnerTimer) { + this.pendingNewState = runState; + return; + } + + if (runState === NotebookCellRunState.Running) { + this.runToolbar.setActions([this.instantiationService.createInstance(CancelCellAction)]); + } else { + this.runToolbar.setActions([this.instantiationService.createInstance(ExecuteCellAction)]); + } + + if (runState === NotebookCellRunState.Success) { + this.element.innerHTML = renderCodicons('$(check)'); + } else if (runState === NotebookCellRunState.Error) { + this.element.innerHTML = renderCodicons('$(error)'); + } else if (runState === NotebookCellRunState.Running) { + this.element.innerHTML = renderCodicons('$(sync~spin)'); + + this.spinnerTimer = setTimeout(() => { + this.spinnerTimer = undefined; + if (this.pendingNewState) { + this.renderState(this.pendingNewState); + this.pendingNewState = undefined; + } + }, RunStateRenderer.MIN_SPINNER_TIME); + } else { + this.element.innerHTML = ''; + } + } +} 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 a7005f54669..c541cb9b136 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts @@ -45,14 +45,14 @@ export class CodeCell extends Disposable { const width = this.viewCell.layoutInfo.editorWidth; const lineNum = this.viewCell.lineCount; const lineHeight = this.viewCell.layoutInfo.fontInfo?.lineHeight || 17; - const totalHeight = this.viewCell.layoutInfo.editorHeight === 0 + const editorHeight = this.viewCell.layoutInfo.editorHeight === 0 ? lineNum * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING : this.viewCell.layoutInfo.editorHeight; this.layoutEditor( { width: width, - height: totalHeight + height: editorHeight } ); @@ -67,7 +67,7 @@ export class CodeCell extends Disposable { } const realContentHeight = templateData.editor?.getContentHeight(); - if (realContentHeight !== undefined && realContentHeight !== totalHeight) { + if (realContentHeight !== undefined && realContentHeight !== editorHeight) { this.onCellHeightChange(realContentHeight); } @@ -84,12 +84,13 @@ export class CodeCell extends Disposable { DOM.toggleClass(templateData.container, 'cell-editor-focus', viewCell.focusMode === CellFocusMode.Editor); }; + const updateForCollapseState = () => { + this.viewUpdate(); + }; this._register(viewCell.onDidChangeState((e) => { - if (!e.focusModeChanged) { - return; + if (e.focusModeChanged) { + updateForFocusMode(); } - - updateForFocusMode(); })); updateForFocusMode(); @@ -97,26 +98,27 @@ export class CodeCell extends Disposable { this._register(viewCell.onDidChangeState((e) => { if (e.metadataChanged) { templateData.editor?.updateOptions({ readOnly: !(viewCell.getEvaluatedMetadata(notebookEditor.viewModel!.metadata).editable) }); + + // TODO@rob this isn't nice + this.viewCell.layoutChange({}); + updateForCollapseState(); + this.relayoutCell(); } })); this._register(viewCell.onDidChangeState((e) => { - if (!e.languageChanged) { - return; + if (e.languageChanged) { + const mode = this._modeService.create(viewCell.language); + templateData.editor?.getModel()?.setMode(mode.languageIdentifier); } - - const mode = this._modeService.create(viewCell.language); - templateData.editor?.getModel()?.setMode(mode.languageIdentifier); })); this._register(viewCell.onDidChangeLayout((e) => { - if (e.outerWidth === undefined) { - return; - } - - const layoutInfo = templateData.editor!.getLayoutInfo(); - if (layoutInfo.width !== viewCell.layoutInfo.editorWidth) { - this.onCellWidthChange(); + if (e.outerWidth !== undefined) { + const layoutInfo = templateData.editor!.getLayoutInfo(); + if (layoutInfo.width !== viewCell.layoutInfo.editorWidth) { + this.onCellWidthChange(); + } } })); @@ -154,13 +156,13 @@ export class CodeCell extends Disposable { this.templateData.outputContainer!.style.display = 'none'; } - let reversedSplices = splices.reverse(); + const reversedSplices = splices.reverse(); reversedSplices.forEach(splice => { viewCell.spliceOutputHeights(splice[0], splice[1], splice[2].map(_ => 0)); }); - let removedKeys: IProcessedOutput[] = []; + const removedKeys: IProcessedOutput[] = []; this.outputElements.forEach((value, key) => { if (viewCell.outputs.indexOf(key) < 0) { @@ -189,12 +191,12 @@ export class CodeCell extends Disposable { } // newly added element - let currIndex = this.viewCell.outputs.indexOf(output); + const currIndex = this.viewCell.outputs.indexOf(output); this.renderOutput(output, currIndex, prevElement); prevElement = this.outputElements.get(output)!.element; }); - let editorHeight = templateData.editor!.getContentHeight(); + const editorHeight = templateData.editor!.getContentHeight(); viewCell.editorHeight = editorHeight; if (previousOutputHeight === 0 || this.viewCell.outputs.length === 0) { @@ -249,6 +251,14 @@ export class CodeCell extends Disposable { } }); + this._register(templateData.editor!.onMouseDown(e => { + // prevent default on right mouse click, otherwise it will trigger unexpected focus changes + // the catch is, it means we don't allow customization of right button mouse down handlers other than the built in ones. + if (e.event.rightButton) { + e.event.preventDefault(); + } + })); + const updateFocusMode = () => viewCell.focusMode = templateData.editor!.hasWidgetFocus() ? CellFocusMode.Editor : CellFocusMode.Container; this._register(templateData.editor!.onDidFocusEditorWidget(() => { updateFocusMode(); @@ -262,7 +272,7 @@ export class CodeCell extends Disposable { if (viewCell.outputs.length > 0) { let layoutCache = false; - if (this.viewCell.layoutInfo.totalHeight !== 0 && this.viewCell.layoutInfo.totalHeight > totalHeight) { + if (this.viewCell.layoutInfo.totalHeight !== 0 && this.viewCell.layoutInfo.editorHeight > editorHeight) { layoutCache = true; this.relayoutCell(); } @@ -277,7 +287,7 @@ export class CodeCell extends Disposable { this.renderOutput(currOutput, index, undefined); } - viewCell.editorHeight = totalHeight; + viewCell.editorHeight = editorHeight; if (layoutCache) { this.relayoutCellDebounced(); } else { @@ -285,10 +295,100 @@ export class CodeCell extends Disposable { } } else { // noop - viewCell.editorHeight = totalHeight; + viewCell.editorHeight = editorHeight; this.relayoutCell(); this.templateData.outputContainer!.style.display = 'none'; } + + // Need to do this after the intial renderOutput + updateForCollapseState(); + } + + private viewUpdate(): void { + if (this.viewCell.metadata?.inputCollapsed && this.viewCell.metadata.outputCollapsed) { + this.viewUpdateAllCollapsed(); + } else if (this.viewCell.metadata?.inputCollapsed) { + this.viewUpdateInputCollapsed(); + } else if (this.viewCell.metadata?.outputCollapsed && this.viewCell.outputs.length) { + this.viewUpdateOutputCollapsed(); + } else { + this.viewUpdateExpanded(); + } + } + + private viewUpdateShowOutputs(): void { + for (let index = 0; index < this.viewCell.outputs.length; index++) { + const currOutput = this.viewCell.outputs[index]; + + 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); + } else { + // Anything else, just update the height + this.viewCell.updateOutputHeight(index, renderedOutput.element.clientHeight); + } + } + } + + this.relayoutCell(); + } + + private viewUpdateInputCollapsed(): void { + DOM.hide(this.templateData.cellContainer); + DOM.show(this.templateData.collapsedPart); + DOM.show(this.templateData.outputContainer); + this.templateData.container.classList.toggle('collapsed', true); + + this.viewUpdateShowOutputs(); + + this.relayoutCell(); + } + + private viewUpdateHideOuputs(): void { + for (const e of this.outputElements.keys()) { + this.notebookEditor.hideInset(e); + } + } + + private viewUpdateOutputCollapsed(): void { + DOM.show(this.templateData.cellContainer); + DOM.show(this.templateData.collapsedPart); + DOM.hide(this.templateData.outputContainer); + + this.viewUpdateHideOuputs(); + + this.templateData.container.classList.toggle('collapsed', false); + this.templateData.container.classList.toggle('output-collapsed', true); + + this.relayoutCell(); + } + + private viewUpdateAllCollapsed(): void { + DOM.hide(this.templateData.cellContainer); + DOM.show(this.templateData.collapsedPart); + DOM.hide(this.templateData.outputContainer); + this.templateData.container.classList.toggle('collapsed', true); + this.templateData.container.classList.toggle('output-collapsed', true); + + for (const e of this.outputElements.keys()) { + this.notebookEditor.hideInset(e); + } + + this.relayoutCell(); + } + + private viewUpdateExpanded(): void { + DOM.show(this.templateData.cellContainer); + DOM.hide(this.templateData.collapsedPart); + DOM.show(this.templateData.outputContainer); + this.templateData.container.classList.toggle('collapsed', false); + this.templateData.container.classList.toggle('output-collapsed', false); + + this.viewUpdateShowOutputs(); + + this.relayoutCell(); } private layoutEditor(dimension: IDimension): void { @@ -328,16 +428,16 @@ export class CodeCell extends Disposable { ); } - renderOutput(currOutput: IProcessedOutput, index: number, beforeElement?: HTMLElement) { + private renderOutput(currOutput: IProcessedOutput, index: number, beforeElement?: HTMLElement) { if (!this.outputResizeListeners.has(currOutput)) { this.outputResizeListeners.set(currOutput, new DisposableStore()); } - let outputItemDiv = document.createElement('div'); + const outputItemDiv = document.createElement('div'); let result: IRenderOutput | undefined = undefined; if (currOutput.outputKind === CellOutputKind.Rich) { - let transformedDisplayOutput = currOutput as ITransformedDisplayOutputDto; + const transformedDisplayOutput = currOutput as ITransformedDisplayOutputDto; if (transformedDisplayOutput.orderedMimeTypes!.length > 1) { outputItemDiv.style.position = 'relative'; @@ -364,7 +464,7 @@ export class CodeCell extends Disposable { }))); } - let pickedMimeTypeRenderer = currOutput.orderedMimeTypes![currOutput.pickedMimeTypeIndex!]; + const pickedMimeTypeRenderer = currOutput.orderedMimeTypes![currOutput.pickedMimeTypeIndex!]; const innerContainer = DOM.$('.output-inner-container'); DOM.append(outputItemDiv, innerContainer); @@ -405,19 +505,19 @@ export class CodeCell extends Disposable { outputItemDiv.style.position = 'absolute'; } - let hasDynamicHeight = result.hasDynamicHeight; + const hasDynamicHeight = result.hasDynamicHeight; if (hasDynamicHeight) { this.viewCell.selfSizeMonitoring = true; - let clientHeight = outputItemDiv.clientHeight; - let dimension = { + const clientHeight = outputItemDiv.clientHeight; + const dimension = { width: this.viewCell.layoutInfo.editorWidth, height: clientHeight }; const elementSizeObserver = getResizesObserver(outputItemDiv, dimension, () => { if (this.templateData.outputContainer && document.body.contains(this.templateData.outputContainer!)) { - let height = Math.ceil(elementSizeObserver.getHeight()); + const height = Math.ceil(elementSizeObserver.getHeight()); if (clientHeight === height) { return; @@ -441,7 +541,7 @@ export class CodeCell extends Disposable { // noop } else { // static output - let clientHeight = Math.ceil(outputItemDiv.clientHeight); + const clientHeight = Math.ceil(outputItemDiv.clientHeight); this.viewCell.updateOutputHeight(index, clientHeight); const top = this.viewCell.getOutputOffsetInContainer(index); @@ -455,7 +555,7 @@ export class CodeCell extends Disposable { return nls.localize('builtinRenderInfo', "built-in"); } - let renderInfo = this.notebookService.getRendererInfo(renderId); + const renderInfo = this.notebookService.getRendererInfo(renderId); if (renderInfo) { return `${renderId} (${renderInfo.extensionId.value})`; @@ -465,7 +565,7 @@ export class CodeCell extends Disposable { } async pickActiveMimeTypeRenderer(output: ITransformedDisplayOutputDto) { - let currIndex = output.pickedMimeTypeIndex; + const currIndex = output.pickedMimeTypeIndex; const items = output.orderedMimeTypes!.map((mimeType, index): IMimeTypeRenderer => ({ label: mimeType.mimeType, id: mimeType.mimeType, @@ -494,10 +594,10 @@ export class CodeCell extends Disposable { if (pick !== currIndex) { // user chooses another mimetype - let index = this.viewCell.outputs.indexOf(output); - let nextElement = index + 1 < this.viewCell.outputs.length ? this.outputElements.get(this.viewCell.outputs[index + 1])?.element : undefined; + const index = this.viewCell.outputs.indexOf(output); + const nextElement = index + 1 < this.viewCell.outputs.length ? this.outputElements.get(this.viewCell.outputs[index + 1])?.element : undefined; this.outputResizeListeners.get(output)?.clear(); - let element = this.outputElements.get(output)?.element; + const element = this.outputElements.get(output)?.element; if (element) { this.templateData?.outputContainer?.removeChild(element); this.notebookEditor.removeInset(output); @@ -547,7 +647,7 @@ export class CodeCell extends Disposable { value.dispose(); }); - this.templateData.focusIndicator!.style.height = 'initial'; + this.templateData.focusIndicatorLeft!.style.height = 'initial'; super.dispose(); } 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 71f4a39cf0c..461c388c8d4 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { hide, IDimension, show, toggleClass, addClass, removeClass } from 'vs/base/browser/dom'; +import * as DOM from 'vs/base/browser/dom'; import { raceCancellation } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { renderCodicons } from 'vs/base/common/codicons'; @@ -21,7 +21,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver'; -export class StatefullMarkdownCell extends Disposable { +export class StatefulMarkdownCell extends Disposable { private editor: CodeEditorWidget | null = null; private markdownContainer: HTMLElement; @@ -55,6 +55,10 @@ export class StatefullMarkdownCell extends Disposable { } })); + this._register(viewCell.model.onDidChangeMetadata(() => { + this.viewUpdate(); + })); + this._register(getResizesObserver(this.markdownContainer, undefined, () => { if (viewCell.editState === CellEditState.Preview) { this.viewCell.renderedMarkdownHeight = templateData.container.clientHeight; @@ -66,7 +70,7 @@ export class StatefullMarkdownCell extends Disposable { this.focusEditorIfNeeded(); } - toggleClass(templateData.container, 'cell-editor-focus', viewCell.focusMode === CellFocusMode.Editor); + templateData.container.classList.toggle('cell-editor-focus', viewCell.focusMode === CellFocusMode.Editor); }; this._register(viewCell.onDidChangeState((e) => { if (!e.focusModeChanged) { @@ -95,7 +99,7 @@ export class StatefullMarkdownCell extends Disposable { this._register(viewCell.onDidChangeLayout((e) => { const layoutInfo = this.editor?.getLayoutInfo(); - if (e.outerWidth && layoutInfo && layoutInfo.width !== viewCell.layoutInfo.editorWidth) { + if (e.outerWidth && this.viewCell.editState === CellEditState.Editing && layoutInfo && layoutInfo.width !== viewCell.layoutInfo.editorWidth) { this.onCellEditorWidthChange(); } else if (e.totalHeight || e.outerWidth) { this.relayoutCell(); @@ -105,13 +109,13 @@ export class StatefullMarkdownCell extends Disposable { this._register(viewCell.onCellDecorationsChanged((e) => { e.added.forEach(options => { if (options.className) { - addClass(templateData.container, options.className); + DOM.addClass(templateData.container, options.className); } }); e.removed.forEach(options => { if (options.className) { - removeClass(templateData.container, options.className); + DOM.removeClass(templateData.container, options.className); } }); })); @@ -120,7 +124,7 @@ export class StatefullMarkdownCell extends Disposable { viewCell.getCellDecorations().forEach(options => { if (options.className) { - addClass(templateData.container, options.className); + DOM.addClass(templateData.container, options.className); } }); @@ -128,19 +132,32 @@ export class StatefullMarkdownCell extends Disposable { } private viewUpdate(): void { - if (this.viewCell.editState === CellEditState.Editing) { + if (this.viewCell.metadata?.inputCollapsed) { + this.viewUpdateCollapsed(); + } else if (this.viewCell.editState === CellEditState.Editing) { this.viewUpdateEditing(); } else { this.viewUpdatePreview(); } } + private viewUpdateCollapsed(): void { + DOM.show(this.templateData.collapsedPart); + DOM.hide(this.editorPart); + DOM.hide(this.markdownContainer); + this.templateData.container.classList.toggle('collapsed', true); + this.viewCell.renderedMarkdownHeight = 0; + } + private viewUpdateEditing(): void { // switch to editing mode let editorHeight: number; - show(this.editorPart); - hide(this.markdownContainer); + DOM.show(this.editorPart); + DOM.hide(this.markdownContainer); + DOM.hide(this.templateData.collapsedPart); + this.templateData.container.classList.toggle('collapsed', false); + if (this.editor) { editorHeight = this.editor!.getContentHeight(); @@ -172,7 +189,8 @@ export class StatefullMarkdownCell extends Disposable { dimension: { width: width, height: editorHeight - } + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() }, {}); this.templateData.currentEditor = this.editor; @@ -216,14 +234,17 @@ export class StatefullMarkdownCell extends Disposable { private viewUpdatePreview(): void { this.viewCell.detachTextEditor(); - hide(this.editorPart); - show(this.markdownContainer); + DOM.hide(this.editorPart); + DOM.hide(this.templateData.collapsedPart); + DOM.show(this.markdownContainer); + this.templateData.container.classList.toggle('collapsed', false); + this.renderedEditors.delete(this.viewCell); this.markdownContainer.innerHTML = ''; this.viewCell.clearHTML(); - let markdownRenderer = this.viewCell.getMarkdownRenderer(); - let renderedHTML = this.viewCell.getHTML(); + const markdownRenderer = this.viewCell.getMarkdownRenderer(); + const renderedHTML = this.viewCell.getHTML(); if (renderedHTML) { this.markdownContainer.appendChild(renderedHTML); } @@ -242,7 +263,7 @@ export class StatefullMarkdownCell extends Disposable { this.localDisposables.add(this.viewCell.textBuffer.onDidChangeContent(() => { this.markdownContainer.innerHTML = ''; this.viewCell.clearHTML(); - let renderedHTML = this.viewCell.getHTML(); + const renderedHTML = this.viewCell.getHTML(); if (renderedHTML) { this.markdownContainer.appendChild(renderedHTML); } @@ -259,7 +280,7 @@ export class StatefullMarkdownCell extends Disposable { } } - private layoutEditor(dimension: IDimension): void { + private layoutEditor(dimension: DOM.IDimension): void { this.editor?.layout(dimension); this.templateData.statusBarContainer.style.width = `${dimension.width}px`; } @@ -307,7 +328,7 @@ export class StatefullMarkdownCell extends Disposable { private bindEditorListeners() { this.localDisposables.add(this.editor!.onDidContentSizeChange(e => { - let viewLayout = this.editor!.getLayoutInfo(); + const viewLayout = this.editor!.getLayoutInfo(); if (e.contentHeightChanged) { this.viewCell.editorHeight = e.contentHeight; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver.ts index 6a5b6ba6041..d9be462c054 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver.ts @@ -33,7 +33,7 @@ export class BrowserResizeObserver extends Disposable implements IResizeObserver this.height = -1; this.observer = new ResizeObserver((entries: any) => { - for (let entry of entries) { + for (const entry of entries) { if (entry.target === referenceDomElement && entry.contentRect) { if (this.width !== entry.contentRect.width || this.height !== entry.contentRect.height) { this.width = entry.contentRect.width; 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 a301e99aa04..7cb5b349f20 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -77,10 +77,10 @@ function webviewPreloads() { const domEval = (container: Element) => { const arr = Array.from(container.getElementsByTagName('script')); for (let n = 0; n < arr.length; n++) { - let node = arr[n]; - let scriptTag = document.createElement('script'); + const node = arr[n]; + const scriptTag = document.createElement('script'); scriptTag.text = node.innerText; - for (let key of preservedScriptAttributes) { + for (const key of preservedScriptAttributes) { const val = node[key] || node.getAttribute && node.getAttribute(key); if (val) { scriptTag.setAttribute(key, val as any); @@ -92,11 +92,11 @@ function webviewPreloads() { } }; - let outputObservers = new Map(); + const outputObservers = new Map(); const resizeObserve = (container: Element, id: string) => { const resizeObserver = new ResizeObserver(entries => { - for (let entry of entries) { + for (const entry of entries) { if (!document.body.contains(entry.target)) { return; } @@ -261,6 +261,8 @@ function webviewPreloads() { interface ICreateCellInfo { outputId: string; + output?: unknown; + mimeType?: string; element: HTMLElement; } @@ -344,14 +346,14 @@ function webviewPreloads() { } let cellOutputContainer = document.getElementById(data.cellId); - let outputId = data.outputId; + const outputId = data.outputId; if (!cellOutputContainer) { const container = document.getElementById('container')!; const upperWrapperElement = createFocusSink(data.cellId, outputId); container.appendChild(upperWrapperElement); - let newElement = document.createElement('div'); + const newElement = document.createElement('div'); newElement.id = data.cellId; container.appendChild(newElement); @@ -361,7 +363,7 @@ function webviewPreloads() { container.appendChild(lowerWrapperElement); } - let outputNode = document.createElement('div'); + const outputNode = document.createElement('div'); outputNode.style.position = 'absolute'; outputNode.style.top = data.top + 'px'; outputNode.style.left = data.left + 'px'; @@ -370,14 +372,25 @@ function webviewPreloads() { outputNode.id = outputId; addMouseoverListeners(outputNode, outputId); - let content = data.content; + 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 { } + } + // eval domEval(outputNode); resizeObserve(outputNode, outputId); - onDidCreateOutput.fire([data.apiNamespace, { element: outputNode, outputId }]); + onDidCreateOutput.fire([data.apiNamespace, { + element: outputNode, + output: pureData?.output, + mimeType: pureData?.mimeType, + outputId + }]); vscode.postMessage({ __vscode_notebook_message: true, @@ -398,9 +411,11 @@ function webviewPreloads() { // console.log('----- will scroll ---- ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds()); for (let i = 0; i < event.data.widgets.length; i++) { - let widget = document.getElementById(event.data.widgets[i].id)!; + const widget = document.getElementById(event.data.widgets[i].id)!; widget.style.top = event.data.widgets[i].top + 'px'; - widget.parentElement!.style.display = 'block'; + if (event.data.forceDisplay) { + widget.parentElement!.style.display = 'block'; + } } break; } @@ -415,7 +430,7 @@ function webviewPreloads() { outputObservers.clear(); break; case 'clearOutput': - let output = document.getElementById(event.data.outputId); + const output = document.getElementById(event.data.outputId); queuedOuputActions.delete(event.data.outputId); // stop any in-progress rendering if (output && output.parentNode) { onWillDestroyOutput.fire([event.data.apiNamespace, { outputId: event.data.outputId }]); @@ -432,16 +447,25 @@ function webviewPreloads() { break; case 'showOutput': enqueueOutputAction(event.data, ({ outputId, top }) => { - let output = document.getElementById(outputId); + const output = document.getElementById(outputId); if (output) { output.parentElement!.style.display = 'block'; output.style.top = top + 'px'; + + vscode.postMessage({ + __vscode_notebook_message: true, + type: 'dimension', + id: outputId, + data: { + height: output.clientHeight + } + }); } }); break; case 'preload': - let resources = event.data.resources; - let preloadsContainer = document.getElementById('__vscode_preloads')!; + const resources = event.data.resources; + const preloadsContainer = document.getElementById('__vscode_preloads')!; for (let i = 0; i < resources.length; i++) { const { uri } = resources[i]; const scriptTag = document.createElement('script'); @@ -458,7 +482,7 @@ function webviewPreloads() { break; case 'decorations': { - let outputContainer = document.getElementById(event.data.cellId); + const outputContainer = document.getElementById(event.data.cellId); event.data.addedClassNames.forEach(n => { outputContainer?.classList.add(n); }); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 5a61751216e..d70f6e4c842 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -61,15 +60,6 @@ export abstract class BaseCellViewModel extends Disposable { } } - private _currentTokenSource: CancellationTokenSource | undefined; - public set currentTokenSource(v: CancellationTokenSource | undefined) { - this._currentTokenSource = v; - } - - public get currentTokenSource(): CancellationTokenSource | undefined { - return this._currentTokenSource; - } - private _focusMode: CellFocusMode = CellFocusMode.Container; get focusMode() { return this._focusMode; @@ -182,7 +172,7 @@ export abstract class BaseCellViewModel extends Disposable { this.saveViewState(); // decorations need to be cleared first as editors can be resued. this._resolvedDecorations.forEach(value => { - let resolvedid = value.id; + const resolvedid = value.id; if (resolvedid) { this._textEditor?.deltaDecorations([resolvedid], []); @@ -308,7 +298,9 @@ export abstract class BaseCellViewModel extends Disposable { } setSelections(selections: Selection[]) { - this._textEditor?.setSelections(selections); + if (selections.length) { + this._textEditor?.setSelections(selections); + } } getSelections() { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index cc709565f8b..306c9722c8d 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -8,15 +8,15 @@ 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, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFindMatch, CodeCellLayoutChangeEvent, CodeCellLayoutInfo, ICellViewModel, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +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 { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; export class CodeCellViewModel extends BaseCellViewModel implements ICellViewModel { - cellKind: CellKind.Code = CellKind.Code; + readonly cellKind = CellKind.Code; protected readonly _onDidChangeOutputs = new Emitter(); readonly onDidChangeOutputs = this._onDidChangeOutputs.event; private _outputCollection: number[] = []; @@ -45,7 +45,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod } get editorHeight() { - return this._editorHeight; + throw new Error('editorHeight is write-only'); } private _hoveringOutput: boolean = false; @@ -87,7 +87,8 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod outputTotalHeight: 0, totalHeight: 0, indicatorHeight: 0, - bottomToolbarOffset: 0 + bottomToolbarOffset: 0, + layoutState: CodeCellLayoutState.Uninitialized }; } @@ -98,22 +99,64 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod layoutChange(state: CodeCellLayoutChangeEvent) { // recompute this._ensureOutputsTop(); - const outputTotalHeight = this._outputsTop!.getTotalValue(); - const totalHeight = EDITOR_TOOLBAR_HEIGHT + this.editorHeight + EDITOR_TOP_MARGIN + outputTotalHeight + BOTTOM_CELL_TOOLBAR_HEIGHT + CELL_STATUSBAR_HEIGHT + CELL_BOTTOM_MARGIN; - const indicatorHeight = this.editorHeight + CELL_STATUSBAR_HEIGHT + outputTotalHeight; - const outputContainerOffset = EDITOR_TOOLBAR_HEIGHT + EDITOR_TOP_MARGIN + this.editorHeight + CELL_STATUSBAR_HEIGHT; - const bottomToolbarOffset = totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT; - const editorWidth = state.outerWidth !== undefined ? this.computeEditorWidth(state.outerWidth) : this._layoutInfo?.editorWidth; - this._layoutInfo = { - fontInfo: state.font || null, - editorHeight: this._editorHeight, - editorWidth, - outputContainerOffset, - outputTotalHeight, - totalHeight, - indicatorHeight, - bottomToolbarOffset: bottomToolbarOffset - }; + let outputTotalHeight = this.metadata?.outputCollapsed ? COLLAPSED_INDICATOR_HEIGHT : this._outputsTop!.getTotalValue(); + + if (!this.metadata?.inputCollapsed) { + let newState: CodeCellLayoutState; + let editorHeight: number; + let totalHeight: number; + if (!state.editorHeight && this._layoutInfo.layoutState === CodeCellLayoutState.FromCache) { + // No new editorHeight info - keep cached totalHeight and estimate editorHeight + editorHeight = this.estimateEditorHeight(state.font?.lineHeight); + totalHeight = this._layoutInfo.totalHeight; + newState = CodeCellLayoutState.FromCache; + } else if (state.editorHeight || this._layoutInfo.layoutState === CodeCellLayoutState.Measured) { + // Editor has been measured + editorHeight = this._editorHeight; + totalHeight = this.computeTotalHeight(this._editorHeight, outputTotalHeight); + newState = CodeCellLayoutState.Measured; + } else { + editorHeight = this.estimateEditorHeight(state.font?.lineHeight); + totalHeight = this.computeTotalHeight(editorHeight, outputTotalHeight); + 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 editorWidth = state.outerWidth !== undefined ? this.computeEditorWidth(state.outerWidth) : this._layoutInfo?.editorWidth; + + this._layoutInfo = { + fontInfo: state.font || null, + editorHeight, + editorWidth, + outputContainerOffset, + outputTotalHeight, + totalHeight, + indicatorHeight, + bottomToolbarOffset, + layoutState: newState + }; + } else { + 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 editorWidth = state.outerWidth !== undefined ? this.computeEditorWidth(state.outerWidth) : this._layoutInfo?.editorWidth; + + this._layoutInfo = { + fontInfo: state.font || null, + editorHeight: this._layoutInfo.editorHeight, + editorWidth, + outputContainerOffset, + outputTotalHeight, + totalHeight, + indicatorHeight, + bottomToolbarOffset, + layoutState: this._layoutInfo.layoutState + }; + } if (state.editorHeight || state.outputHeight) { state.totalHeight = true; @@ -128,7 +171,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod restoreEditorViewState(editorViewStates: editorCommon.ICodeEditorViewState | null, totalHeight?: number) { super.restoreEditorViewState(editorViewStates); - if (totalHeight !== undefined) { + if (totalHeight !== undefined && this._layoutInfo.layoutState !== CodeCellLayoutState.Measured) { this._layoutInfo = { fontInfo: this._layoutInfo.fontInfo, editorHeight: this._layoutInfo.editorHeight, @@ -137,36 +180,38 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod outputTotalHeight: this._layoutInfo.outputTotalHeight, totalHeight: totalHeight, indicatorHeight: this._layoutInfo.indicatorHeight, - bottomToolbarOffset: this._layoutInfo.bottomToolbarOffset + bottomToolbarOffset: this._layoutInfo.bottomToolbarOffset, + layoutState: CodeCellLayoutState.FromCache }; } } hasDynamicHeight() { - if (this.selfSizeMonitoring) { - // if there is an output rendered in the webview, it should always be false - return false; - } + // CodeCellVM always measures itself and controls its cell's height + return false; + } - if (this.outputs && this.outputs.length > 0) { - // if it contains output, it will be marked as dynamic height - // thus when it's being rendered, the list view will `probeHeight` - // inside which, we will check domNode's height directly instead of doing another `renderElement` with height undefined. - return true; - } - else { - return false; - } + firstLine(): string { + return this.getText().split('\n')[0]; } getHeight(lineHeight: number) { - if (this._layoutInfo.totalHeight === 0) { - return EDITOR_TOOLBAR_HEIGHT + EDITOR_TOP_MARGIN + this.lineCount * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING + BOTTOM_CELL_TOOLBAR_HEIGHT; + if (this._layoutInfo.layoutState === CodeCellLayoutState.Uninitialized) { + const editorHeight = this.estimateEditorHeight(lineHeight); + return this.computeTotalHeight(editorHeight, 0); } else { return this._layoutInfo.totalHeight; } } + private estimateEditorHeight(lineHeight: number | undefined = 20): number { + return this.lineCount * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; + } + + 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; + } + /** * Text model is used for editing. */ @@ -195,8 +240,9 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod this._outputCollection[index] = height; this._ensureOutputsTop(); - this._outputsTop!.changeValue(index, height); - this.layoutChange({ outputHeight: true }); + if (this._outputsTop!.changeValue(index, height)) { + this.layoutChange({ outputHeight: true }); + } } getOutputOffsetInContainer(index: number) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts index 6b45a999f89..8700ba68862 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts @@ -54,7 +54,7 @@ export class NotebookEventDispatcher { emit(events: NotebookViewEvent[]) { for (let i = 0, len = events.length; i < len; i++) { - let e = events[i]; + const e = events[i]; switch (e.type) { case NotebookViewEventType.LayoutChanged: diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts index c63501ade8b..9c1dc125163 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts @@ -8,7 +8,7 @@ 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, EDITOR_TOP_MARGIN, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; +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 { 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'; @@ -18,7 +18,7 @@ import { CellKind, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/ import { NotebookEventDispatcher, NotebookCellStateChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; export class MarkdownCellViewModel extends BaseCellViewModel implements ICellViewModel { - cellKind: CellKind.Markdown = CellKind.Markdown; + readonly cellKind = CellKind.Markdown; private _html: HTMLElement | null = null; private _layoutInfo: MarkdownCellLayoutInfo; @@ -45,7 +45,7 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie set editorHeight(newHeight: number) { this._editorHeight = newHeight; - this.totalHeight = this._editorHeight + EDITOR_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_HEIGHT + CELL_STATUSBAR_HEIGHT; } get editorHeight() { @@ -92,15 +92,31 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie layoutChange(state: MarkdownCellLayoutChangeEvent) { // recompute - const editorWidth = state.outerWidth !== undefined ? this.computeEditorWidth(state.outerWidth) : this._layoutInfo.editorWidth; - this._layoutInfo = { - fontInfo: state.font || this._layoutInfo.fontInfo, - editorWidth, - editorHeight: this._editorHeight, - bottomToolbarOffset: BOTTOM_CELL_TOOLBAR_HEIGHT, - totalHeight: state.totalHeight === undefined ? this._layoutInfo.totalHeight : state.totalHeight - }; + if (!this.metadata?.inputCollapsed) { + const editorWidth = state.outerWidth !== undefined ? this.computeEditorWidth(state.outerWidth) : this._layoutInfo.editorWidth; + const totalHeight = state.totalHeight === undefined ? this._layoutInfo.totalHeight : state.totalHeight; + + this._layoutInfo = { + fontInfo: state.font || this._layoutInfo.fontInfo, + editorWidth, + editorHeight: this._editorHeight, + bottomToolbarOffset: totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT - BOTTOM_CELL_TOOLBAR_OFFSET, + 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; + 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, + totalHeight + }; + } this._onDidChangeLayout.fire(state); } @@ -115,6 +131,7 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie totalHeight: totalHeight, editorHeight: this._editorHeight }; + this.layoutChange({}); } } @@ -139,7 +156,7 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie if (this._html) { return this._html; } - let renderer = this.getMarkdownRenderer(); + const renderer = this.getMarkdownRenderer(); const text = this.getText(); if (text.length === 0) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index 12cfb497361..47739daac44 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -33,6 +32,7 @@ import { IPosition, Position } from 'vs/editor/common/core/position'; import { SplitCellEdit, JoinCellEdit } from 'vs/workbench/contrib/notebook/browser/viewModel/cellEdit'; import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel'; import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer'; +import { MultiModelEditStackElement, SingleModelEditStackElement } from 'vs/editor/common/model/editStack'; export interface INotebookEditorViewState { editingCells: { [key: number]: boolean }; @@ -154,16 +154,6 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD private _viewCells: CellViewModel[] = []; private _handleToViewCellMapping = new Map(); - private _currentTokenSource: CancellationTokenSource | undefined; - - get currentTokenSource(): CancellationTokenSource | undefined { - return this._currentTokenSource; - } - - set currentTokenSource(v: CancellationTokenSource | undefined) { - this._currentTokenSource = v; - } - get viewCells(): ICellViewModel[] { return this._viewCells; } @@ -359,6 +349,16 @@ 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; } @@ -397,7 +397,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD updateFoldingRanges(ranges: FoldingRegions) { this._foldingRanges = ranges; let updateHiddenAreas = false; - let newHiddenAreas: ICellRange[] = []; + const newHiddenAreas: ICellRange[] = []; let i = 0; // index into hidden let k = 0; @@ -410,8 +410,8 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD continue; } - let startLineNumber = ranges.getStartLineNumber(i) + 1; // the first line is not hidden - let endLineNumber = ranges.getEndLineNumber(i); + const startLineNumber = ranges.getStartLineNumber(i) + 1; // the first line is not hidden + const endLineNumber = ranges.getEndLineNumber(i); if (lastCollapsedStart <= startLineNumber && endLineNumber <= lastCollapsedEnd) { // ignore ranges contained in collapsed regions continue; @@ -543,7 +543,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD const newDecorationsLen = newDecorations.length; let newDecorationIndex = 0; - let result = new Array(newDecorationsLen); + const result = new Array(newDecorationsLen); while (oldDecorationIndex < oldDecorationsLen || newDecorationIndex < newDecorationsLen) { let node: IntervalNode | null = null; @@ -608,7 +608,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } }); - let result: string[] = []; + const result: string[] = []; newDecorations.forEach(decoration => { const cell = this.getCellByHandle(decoration.handle); @@ -697,7 +697,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._pushIfAbsent(boundaries, new Position(1, 1)); for (let sp of splitPoints) { - if (getLineLen(sp.lineNumber) + 1 === sp.column && sp.lineNumber < lineCnt) { + if (getLineLen(sp.lineNumber) + 1 === sp.column && sp.column !== 1 /** empty line */ && sp.lineNumber < lineCnt) { sp = new Position(sp.lineNumber + 1, 1); } this._pushIfAbsent(boundaries, sp); @@ -737,7 +737,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return null; } - let splitPoints = cell.getSelectionsStartPosition(); + const splitPoints = cell.getSelectionsStartPosition(); if (splitPoints && splitPoints.length > 0) { await cell.resolveTextModel(); @@ -745,7 +745,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return null; } - let newLinesContents = this._computeCellLinesContents(cell, splitPoints); + const newLinesContents = this._computeCellLinesContents(cell, splitPoints); if (newLinesContents) { const editorSelections = cell.getSelections(); this._notebook.splitNotebookCell(index, newLinesContents, this.selectionHandles); @@ -1042,7 +1042,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return; } - let textEdits: WorkspaceTextEdit[] = []; + const textEdits: WorkspaceTextEdit[] = []; this._lastNotebookEditResource.push(matches[0].cell.uri); matches.forEach(match => { @@ -1062,12 +1062,46 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD }); } + async withElement(element: SingleModelEditStackElement | MultiModelEditStackElement, callback: () => Promise) { + const viewCells = this._viewCells.filter(cell => element.matchesResource(cell.uri)); + const refs = await Promise.all(viewCells.map(cell => cell.model.resolveTextModelRef())); + await callback(); + refs.forEach(ref => ref.dispose()); + } + async undo() { + if (!this.metadata.editable) { + return; + } + + const editStack = this._undoService.getElements(this.uri); + 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._undoService.undo(this.uri); + }); + } + await this._undoService.undo(this.uri); } async redo() { + if (!this.metadata.editable) { + return; + } + + const editStack = this._undoService.getElements(this.uri); + const element = editStack.future[0]; + + if (element && element instanceof SingleModelEditStackElement || element instanceof MultiModelEditStackElement) { + return await this.withElement(element, async () => { + await this._undoService.redo(this.uri); + }); + } + await this._undoService.redo(this.uri); + } 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 2bf2293f03c..0d609102629 100644 --- a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts +++ b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts @@ -31,7 +31,7 @@ export class InsertCellEdit implements IResourceUndoRedoElement { ) { } - undo(): void | Promise { + undo(): void { if (!this.editingDelegate.deleteCell) { throw new Error('Notebook Delete Cell not implemented for Undo/Redo'); } @@ -41,7 +41,7 @@ export class InsertCellEdit implements IResourceUndoRedoElement { this.editingDelegate.emitSelections(this.beforedSelections); } } - redo(): void | Promise { + redo(): void { if (!this.editingDelegate.insertCell) { throw new Error('Notebook Insert Cell not implemented for Undo/Redo'); } @@ -70,7 +70,7 @@ export class DeleteCellEdit implements IResourceUndoRedoElement { // this._rawCell.source = [cell.getText()]; } - undo(): void | Promise { + undo(): void { if (!this.editingDelegate.insertCell) { throw new Error('Notebook Insert Cell not implemented for Undo/Redo'); } @@ -81,7 +81,7 @@ export class DeleteCellEdit implements IResourceUndoRedoElement { } } - redo(): void | Promise { + redo(): void { if (!this.editingDelegate.deleteCell) { throw new Error('Notebook Delete Cell not implemented for Undo/Redo'); } @@ -95,7 +95,7 @@ export class DeleteCellEdit implements IResourceUndoRedoElement { export class MoveCellEdit implements IResourceUndoRedoElement { type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; - label: string = 'Delete Cell'; + label: string = 'Move Cell'; constructor( public resource: URI, @@ -107,7 +107,7 @@ export class MoveCellEdit implements IResourceUndoRedoElement { ) { } - undo(): void | Promise { + undo(): void { if (!this.editingDelegate.moveCell) { throw new Error('Notebook Move Cell not implemented for Undo/Redo'); } @@ -118,7 +118,7 @@ export class MoveCellEdit implements IResourceUndoRedoElement { } } - redo(): void | Promise { + redo(): void { if (!this.editingDelegate.moveCell) { throw new Error('Notebook Move Cell not implemented for Undo/Redo'); } @@ -142,7 +142,7 @@ export class SpliceCellsEdit implements IResourceUndoRedoElement { ) { } - undo(): void | Promise { + undo(): void { if (!this.editingDelegate.deleteCell || !this.editingDelegate.insertCell) { throw new Error('Notebook Insert/Delete Cell not implemented for Undo/Redo'); } @@ -162,7 +162,7 @@ export class SpliceCellsEdit implements IResourceUndoRedoElement { } } - redo(): void | Promise { + redo(): void { if (!this.editingDelegate.deleteCell || !this.editingDelegate.insertCell) { throw new Error('Notebook Insert/Delete Cell not implemented for Undo/Redo'); } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index 015b9db73be..015f62a96e3 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -58,7 +58,7 @@ export class NotebookCellTextModel extends Disposable implements ICell { return this._textBuffer; } - let builder = new PieceTreeTextBufferBuilder(); + 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); diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 925e0144e46..a45d181df50 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -3,13 +3,14 @@ * 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 { 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 { ITextSnapshot } from 'vs/editor/common/model'; -import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +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 { ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -65,6 +66,53 @@ export class NotebookTextModelSnapshot implements ITextSnapshot { } +class StackOperation implements IResourceUndoRedoElement { + type: UndoRedoElementType.Resource; + + private _operations: IUndoRedoElement[] = []; + + constructor(readonly resource: URI, readonly label: string) { + this.type = UndoRedoElementType.Resource; + } + + pushEditOperation(element: IUndoRedoElement) { + this._operations.push(element); + } + + undo(): void { + this._operations.reverse().forEach(o => o.undo()); + } + redo(): void | Promise { + this._operations.forEach(o => o.redo()); + } +} + +export class NotebookOperationManager { + private _pendingStackOperation: StackOperation | null = null; + constructor(private _undoService: IUndoRedoService, private _resource: URI) { + + } + + pushStackElement(label: string) { + if (this._pendingStackOperation) { + this._undoService.pushElement(this._pendingStackOperation); + this._pendingStackOperation = null; + return; + } + + this._pendingStackOperation = new StackOperation(this._resource, label); + } + + pushEditOperation(element: IUndoRedoElement) { + if (this._pendingStackOperation) { + this._pendingStackOperation.pushEditOperation(element); + return; + } + + this._undoService.pushElement(element); + } +} + export class NotebookTextModel extends Disposable implements INotebookTextModel { private _cellhandlePool: number = 0; @@ -111,16 +159,20 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel protected readonly _onDidChangeDirty = this._register(new Emitter()); readonly onDidChangeDirty = this._onDidChangeDirty.event; + private _operationManager: NotebookOperationManager; + constructor( public handle: number, public viewType: string, public supportBackup: boolean, public uri: URI, - private _undoService: IUndoRedoService, - private _modelService: ITextModelService + @IUndoRedoService private _undoService: IUndoRedoService, + @ITextModelService private _modelService: ITextModelService ) { super(); this.cells = []; + + this._operationManager = new NotebookOperationManager(this._undoService, uri); } get isDirty() { @@ -160,7 +212,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel for (let i = 0; i < mainCells.length; i++) { this._mapping.set(mainCells[i].handle, mainCells[i]); - let dirtyStateListener = mainCells[i].onDidChangeContent(() => { + const dirtyStateListener = mainCells[i].onDidChangeContent(() => { this.setDirty(true); this._onDidChangeContent.fire(); }); @@ -172,7 +224,11 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._increaseVersionId(); } - $applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[], emitToExtHost: boolean, synchronous: boolean): boolean { + pushStackElement(label: string) { + this._operationManager.pushStackElement(label); + } + + $applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[], synchronous: boolean): boolean { if (modelVersionId !== this._versionId) { return false; } @@ -203,7 +259,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel // const edits operations = operations.sort((a, b) => { - let r = compareRangesUsingEnds([a.start, a.end], [b.start, b.end]); + const r = compareRangesUsingEnds([a.start, a.end], [b.start, b.end]); if (r === 0) { return b.sortIndex - a.sortIndex; } @@ -233,22 +289,20 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return [diff.start, diff.deleteCount, diff.toInsert] as [number, number, NotebookCellTextModel[]]; }); - if (emitToExtHost) { - this._onDidModelChangeProxy.fire({ - kind: NotebookCellsChangeType.ModelChange, - versionId: this._versionId, - changes: diffs.map(diff => [diff[0], diff[1], diff[2].map(cell => ({ - handle: cell.handle, - uri: cell.uri, - source: cell.textBuffer.getLinesContent(), - eol: cell.textBuffer.getEOL(), - language: cell.language, - cellKind: cell.cellKind, - outputs: cell.outputs, - metadata: cell.metadata - }))] as [number, number, IMainCellDto[]]) - }); - } + this._onDidModelChangeProxy.fire({ + kind: NotebookCellsChangeType.ModelChange, + versionId: this._versionId, + changes: diffs.map(diff => [diff[0], diff[1], diff[2].map(cell => ({ + handle: cell.handle, + uri: cell.uri, + source: cell.textBuffer.getLinesContent(), + eol: cell.textBuffer.getEOL(), + language: cell.language, + cellKind: cell.cellKind, + outputs: cell.outputs, + metadata: cell.metadata + }))] as [number, number, IMainCellDto[]]) + }); const undoDiff = diffs.map(diff => { const deletedCells = this.cells.slice(diff[0], diff[0] + diff[1]); @@ -256,7 +310,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return [diff[0], deletedCells, diff[2]] as [number, NotebookCellTextModel[], NotebookCellTextModel[]]; }); - this._undoService.pushElement(new SpliceCellsEdit(this.uri, undoDiff, { + this._operationManager.pushEditOperation(new SpliceCellsEdit(this.uri, undoDiff, { insertCell: this._insertCellDelegate.bind(this), deleteCell: this._deleteCellDelegate.bind(this), emitSelections: this._emitSelectionsDelegate.bind(this) @@ -266,6 +320,21 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return true; } + $handleEdit(label: string | undefined, undo: () => void, redo: () => void): void { + this._operationManager.pushEditOperation({ + type: UndoRedoElementType.Resource, + resource: this.uri, + label: label ?? nls.localize('defaultEditLabel', "Edit"), + undo: async () => { + undo(); + }, + redo: async () => { + redo(); + }, + }); + this.setDirty(true); + } + createSnapshot(preserveBOM?: boolean): ITextSnapshot { return new NotebookTextModelSnapshot(this); } @@ -315,7 +384,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this.cells = [cell]; this._mapping.set(cell.handle, cell); - let dirtyStateListener = cell.onDidChangeContent(() => { + const dirtyStateListener = cell.onDidChangeContent(() => { this._isUntitled = false; this.setDirty(true); this._onDidChangeContent.fire(); @@ -352,7 +421,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel for (let i = 0; i < cells.length; i++) { this._mapping.set(cells[i].handle, cells[i]); - let dirtyStateListener = cells[i].onDidChangeContent(() => { + const dirtyStateListener = cells[i].onDidChangeContent(() => { this.setDirty(true); this._onDidChangeContent.fire(); }); @@ -394,7 +463,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._isUntitled = false; for (let i = index; i < index + count; i++) { - let cell = this.cells[i]; + const cell = this.cells[i]; this._cellListeners.get(cell.handle)?.dispose(); this._cellListeners.delete(cell.handle); } @@ -432,12 +501,12 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel // TODO@rebornix should this trigger content change event? $spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]): void { - let cell = this._mapping.get(cellHandle); + const cell = this._mapping.get(cellHandle); cell?.spliceNotebookCellOutputs(splices); } clearCellOutput(handle: number) { - let cell = this._mapping.get(handle); + const cell = this._mapping.get(handle); if (cell) { cell.spliceNotebookCellOutputs([ [0, cell.outputs.length, []] @@ -449,7 +518,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } changeCellLanguage(handle: number, languageId: string) { - let cell = this._mapping.get(handle); + const cell = this._mapping.get(handle); if (cell && cell.language !== languageId) { cell.language = languageId; @@ -458,6 +527,19 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } } + changeCellMetadata(handle: number, newMetadata: NotebookCellMetadata) { + const cell = this._mapping.get(handle); + if (cell) { + cell.metadata = { + ...cell.metadata, + ...newMetadata + }; + + this._increaseVersionId(); + this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ChangeMetadata, versionId: this._versionId, index: this.cells.indexOf(cell), metadata: cell.metadata }); + } + } + clearAllCellOutputs() { this.cells.forEach(cell => { cell.spliceNotebookCellOutputs([ @@ -488,7 +570,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const cell = this.createCellTextModel(source, language, type, [], metadata); if (pushUndoStop) { - this._undoService.pushElement(new InsertCellEdit(this.uri, index, cell, { + this._operationManager.pushEditOperation(new InsertCellEdit(this.uri, index, cell, { insertCell: this._insertCellDelegate.bind(this), deleteCell: this._deleteCellDelegate.bind(this), emitSelections: this._emitSelectionsDelegate.bind(this) @@ -508,7 +590,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel insertCell2(index: number, cell: NotebookCellTextModel, synchronous: boolean, pushUndoStop: boolean): void { if (pushUndoStop) { - this._undoService.pushElement(new InsertCellEdit(this.uri, index, cell, { + this._operationManager.pushEditOperation(new InsertCellEdit(this.uri, index, cell, { insertCell: this._insertCellDelegate.bind(this), deleteCell: this._deleteCellDelegate.bind(this), emitSelections: this._emitSelectionsDelegate.bind(this) @@ -522,7 +604,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel deleteCell2(index: number, synchronous: boolean, pushUndoStop: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined) { const cell = this.cells[index]; if (pushUndoStop) { - this._undoService.pushElement(new DeleteCellEdit(this.uri, index, cell, { + this._operationManager.pushEditOperation(new DeleteCellEdit(this.uri, index, cell, { insertCell: this._insertCellDelegate.bind(this), deleteCell: this._deleteCellDelegate.bind(this), emitSelections: this._emitSelectionsDelegate.bind(this) @@ -539,7 +621,7 @@ 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]; if (pushedToUndoStack) { - this._undoService.pushElement(new MoveCellEdit(this.uri, index, newIdx, { + 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); }, diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 5582306d02f..46c9f3f0ec5 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -53,6 +53,11 @@ export const ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER = [ export const BUILTIN_RENDERER_ID = '_builtin'; +export enum NotebookRunState { + Running = 1, + Idle = 2 +} + export const notebookDocumentMetadataDefaults: Required = { editable: true, runnable: true, @@ -60,7 +65,8 @@ export const notebookDocumentMetadataDefaults: Required; + providerHandle?: number; + executeNotebook(viewType: string, uri: URI, handle: number | undefined): Promise; + } export interface INotebookKernelInfoDto { @@ -314,7 +325,8 @@ export enum NotebookCellsChangeType { CellClearOutput = 3, CellsClearOutput = 4, ChangeLanguage = 5, - Initialize = 6 + Initialize = 6, + ChangeMetadata = 7 } export interface NotebookCellsInitializeEvent { @@ -354,7 +366,14 @@ export interface NotebookCellsChangeLanguageEvent { readonly language: string; } -export type NotebookCellsChangedEvent = NotebookCellsInitializeEvent | NotebookCellsModelChangedEvent | NotebookCellsModelMoveEvent | NotebookCellClearOutputEvent | NotebookCellsClearOutputEvent | NotebookCellsChangeLanguageEvent; +export interface NotebookCellsChangeMetadataEvent { + readonly kind: NotebookCellsChangeType.ChangeMetadata; + readonly versionId: number; + readonly index: number; + readonly metadata: NotebookCellMetadata; +} + +export type NotebookCellsChangedEvent = NotebookCellsInitializeEvent | NotebookCellsModelChangedEvent | NotebookCellsModelMoveEvent | NotebookCellClearOutputEvent | NotebookCellsClearOutputEvent | NotebookCellsChangeLanguageEvent | NotebookCellsChangeMetadataEvent; export enum CellEditType { Insert = 1, Delete = 2 @@ -394,6 +413,15 @@ export interface NotebookDataDto { readonly metadata: NotebookDocumentMetadata; } +export function getCellUndoRedoComparisonKey(uri: URI) { + const data = CellUri.parse(uri); + if (!data) { + return uri.toString(); + } + + return data.notebook.toString(); +} + export namespace CellUri { @@ -641,6 +669,7 @@ export interface INotebookKernelInfoDto2 { label: string; extension: ExtensionIdentifier; extensionLocation: URI; + providerHandle?: number; description?: string; isPreferred?: boolean; preloads?: UriComponents[]; @@ -648,13 +677,17 @@ export interface INotebookKernelInfoDto2 { export interface INotebookKernelInfo2 extends INotebookKernelInfoDto2 { resolve(uri: URI, editorId: string, token: CancellationToken): Promise; - executeNotebookCell?(uri: URI, handle: number | undefined, token: CancellationToken): Promise; + executeNotebookCell?(uri: URI, handle: number | undefined): Promise; + cancelNotebookCell?(uri: URI, handle: number | undefined): Promise; } export interface INotebookKernelProvider { + providerExtensionId: string; + providerDescription?: string; selector: INotebookDocumentFilter; 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, token: CancellationToken): Promise; + executeNotebook(uri: URI, kernelId: string, handle: number | undefined): Promise; + cancelNotebook(uri: URI, kernelId: string, handle: number | undefined): Promise; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 35213ab164a..a0e064a2aec 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -14,7 +14,6 @@ import { IWorkingCopyService, IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapab import { basename } from 'vs/base/common/resources'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { DefaultEndOfLine, ITextBuffer, EndOfLinePreference } from 'vs/editor/common/model'; 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'; @@ -142,42 +141,9 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN return this; // Make sure meanwhile someone else did not succeed in loading } - if (backup && backup.meta?.backupId === undefined) { - try { - return await this.loadFromBackup(backup.value.create(DefaultEndOfLine.LF), options?.editorId); - } catch (error) { - // this.logService.error('[text file model] load() from backup', error); // ignore error and continue to load as file below - } - } - return this.loadFromProvider(false, options?.editorId, backup?.meta?.backupId); } - private async loadFromBackup(content: ITextBuffer, editorId?: string): Promise { - const fullRange = content.getRangeAt(0, content.getLength()); - const data = JSON.parse(content.getValueInRange(fullRange, EndOfLinePreference.LF)); - - const notebook = await this._notebookService.createNotebookFromBackup(this.viewType!, this.resource, data.metadata, data.languages, data.cells, editorId); - this._notebook = notebook!; - 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(); - })); - this._register(this._notebook.onDidChangeDirty(() => { - this._onDidChangeDirty.fire(); - })); - - await this._backupFileService.discardBackup(this._workingCopyResource); - this._notebook.setDirty(true); - - return this; - } - 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!; diff --git a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts index aa484dbfbaa..b22c64145eb 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts @@ -24,7 +24,7 @@ export class NotebookOutputRendererInfo { } matches(mimeType: string) { - let matched = this.mimeTypeGlobs.find(pattern => pattern(mimeType)); + const matched = this.mimeTypeGlobs.find(pattern => pattern(mimeType)); if (matched) { return true; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts index 7756d95221d..2ffcad9a909 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts @@ -18,7 +18,8 @@ export interface NotebookEditorDescriptor { readonly displayName: string; readonly selector: readonly NotebookSelector[]; readonly priority: NotebookEditorPriority; - readonly providerId?: string; + readonly providerExtensionId?: string; + readonly providerDescription?: string; readonly providerDisplayName: string; readonly providerExtensionLocation: URI; kernel?: INotebookKernelInfoDto; @@ -31,7 +32,8 @@ export class NotebookProviderInfo implements NotebookEditorDescriptor { readonly selector: readonly NotebookSelector[]; readonly priority: NotebookEditorPriority; // it's optional as the memento might not have it - readonly providerId?: string; + readonly providerExtensionId?: string; + readonly providerDescription?: string; readonly providerDisplayName: string; readonly providerExtensionLocation: URI; kernel?: INotebookKernelInfoDto; @@ -41,7 +43,8 @@ export class NotebookProviderInfo implements NotebookEditorDescriptor { this.displayName = descriptor.displayName; this.selector = descriptor.selector; this.priority = descriptor.priority; - this.providerId = descriptor.providerId; + this.providerExtensionId = descriptor.providerExtensionId; + this.providerDescription = descriptor.providerDescription; this.providerDisplayName = descriptor.providerDisplayName; this.providerExtensionLocation = descriptor.providerExtensionLocation; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 4ca02503000..9c6d199d7ed 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -9,7 +9,7 @@ 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, NotebookDocumentMetadata, ICellDto2, INotebookKernelInfo, INotebookKernelInfoDto, INotebookTextModelBackup, + INotebookTextModel, INotebookRendererInfo, INotebookKernelInfo, INotebookKernelInfoDto, IEditor, ICellEditOperation, NotebookCellOutputsSplice, IOrderedMimeType, IProcessedOutput, INotebookKernelProvider, INotebookKernelInfo2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; @@ -22,12 +22,16 @@ export const INotebookService = createDecorator('notebookServi export interface IMainNotebookController { kernel: INotebookKernelInfoDto | undefined; - createNotebook(viewType: string, uri: URI, backup: INotebookTextModelBackup | undefined, forceReload: boolean, editorId?: string, backupId?: string): Promise; + supportBackup: boolean; + 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, token: CancellationToken): 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, token: CancellationToken): Promise; - removeNotebookDocument(notebook: INotebookTextModel): Promise; + 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; backup(uri: URI, token: CancellationToken): Promise; @@ -42,7 +46,9 @@ export interface INotebookService { onNotebookEditorsRemove: Event; onNotebookDocumentRemove: Event; onNotebookDocumentAdd: Event; + onNotebookDocumentSaved: 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; @@ -57,11 +63,13 @@ export interface INotebookService { getContributedNotebookKernels2(viewType: string, resource: URI, token: CancellationToken): Promise; getRendererInfo(id: string): INotebookRendererInfo | undefined; resolveNotebook(viewType: string, uri: URI, forceReload: boolean, editorId?: string, backupId?: string): Promise; - createNotebookFromBackup(viewType: string, uri: URI, metadata: NotebookDocumentMetadata, languages: string[], cells: ICellDto2[], editorId?: string): Promise; - executeNotebook(viewType: string, uri: URI, token: CancellationToken): Promise; - executeNotebookCell(viewType: string, uri: URI, handle: number, token: CancellationToken): Promise; - executeNotebook2(viewType: string, uri: URI, kernelId: string, token: CancellationToken): Promise; - executeNotebookCell2(viewType: string, uri: URI, handle: number, kernelId: string, token: CancellationToken): 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; getContributedNotebookProviders(resource: URI): readonly NotebookProviderInfo[]; getContributedNotebookProvider(viewType: string): NotebookProviderInfo | undefined; getNotebookProviderResourceRoots(): URI[]; 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 d0e8f6b32f5..fe8d01653ca 100644 --- a/src/vs/workbench/contrib/notebook/electron-browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/electron-browser/notebook.contribution.ts @@ -18,6 +18,10 @@ function getFocusedElectronBasedWebviewDelegate(accessor: ServicesAccessor): Ele return; } + if (!editor?.hasWebviewFocus()) { + return; + } + const webview = editor?.getInnerWebview(); if (webview && webview instanceof ElectronWebviewBasedWebview) { return webview; diff --git a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts index a5e5a19039c..23ffd1fb439 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts @@ -32,7 +32,7 @@ suite('NotebookTextModel', () => { 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)] }, - ], true, true); + ], true); assert.equal(textModel.cells.length, 6); @@ -57,7 +57,7 @@ suite('NotebookTextModel', () => { 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)] }, - ], true, true); + ], true); assert.equal(textModel.cells.length, 6); @@ -82,7 +82,7 @@ suite('NotebookTextModel', () => { textModel.$applyEdit(textModel.versionId, [ { editType: CellEditType.Delete, index: 1, count: 1 }, { editType: CellEditType.Delete, index: 3, count: 1 }, - ], true, true); + ], true); assert.equal(textModel.cells[0].getValue(), 'var a = 1;'); assert.equal(textModel.cells[1].getValue(), 'var c = 3;'); @@ -105,7 +105,7 @@ suite('NotebookTextModel', () => { 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)] }, - ], true, true); + ], true); assert.equal(textModel.cells.length, 4); @@ -130,7 +130,7 @@ suite('NotebookTextModel', () => { 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)] }, - ], true, true); + ], true); assert.equal(textModel.cells.length, 4); assert.equal(textModel.cells[0].getValue(), 'var a = 1;'); diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index f979f53344a..60d7d3cfdf7 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -239,7 +239,7 @@ function getVisibleCells(cells: T[], hiddenRanges: ICellRange[]) { let start = 0; let hiddenRangeIndex = 0; - let result: T[] = []; + const result: T[] = []; while (start < cells.length && hiddenRangeIndex < hiddenRanges.length) { if (start < hiddenRanges[hiddenRangeIndex].start) { diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index 63020e5d178..a79ceb86034 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -62,7 +62,13 @@ export class TestNotebookEditor implements INotebookEditor { constructor( ) { } + hideInset(output: IProcessedOutput): void { + throw new Error('Method not implemented.'); + } + multipleKernelsAvailable: boolean = false; + onDidChangeAvailableKernels: Event = new Emitter().event; + uri?: URI | undefined; textModel?: NotebookTextModel | undefined; @@ -75,6 +81,15 @@ export class TestNotebookEditor implements INotebookEditor { hasFocus(): boolean { return true; } + + hasWebviewFocus() { + return false; + } + + hasOutputTextSelection() { + return false; + } + getId(): string { return 'notebook.testEditor'; } @@ -88,6 +103,10 @@ export class TestNotebookEditor implements INotebookEditor { throw new Error('Method not implemented.'); } + getOverflowContainerDomNode(): HTMLElement { + throw new Error('Method not implemented.'); + } + private _onDidChangeModel = new Emitter(); onDidChangeModel: Event = this._onDidChangeModel.event; getContribution(id: string): T { @@ -158,6 +177,10 @@ export class TestNotebookEditor implements INotebookEditor { throw new Error('Method not implemented.'); } + moveCellToIdx(cell: ICellViewModel, index: number): Promise { + throw new Error('Method not implemented.'); + } + moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/src/vs/workbench/contrib/outline/browser/outlinePane.ts index 6c94aa6d7a7..a277a22ed17 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePane.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePane.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; -import { Action, IAction, RadioGroup } from 'vs/base/common/actions'; +import { Action, IAction, RadioGroup, Separator } from 'vs/base/common/actions'; import { createCancelablePromise, TimeoutTimer } from 'vs/base/common/async'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index 7eabcf2130a..2e5bd2cc7eb 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { IAction } from 'vs/base/common/actions'; -import { IActionViewItem, SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IAction, IActionViewItem } from 'vs/base/common/actions'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -37,6 +36,7 @@ import { groupBy } from 'vs/base/common/arrays'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { editorBackground, selectBorder } from 'vs/platform/theme/common/colorRegistry'; import { addClass } from 'vs/base/browser/dom'; +import { SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; export class OutputViewPane extends ViewPane { diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index f2e21d347cb..fc4533b84ea 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -12,8 +12,8 @@ import { dispose, Disposable, IDisposable, combinedDisposable, DisposableStore } import { CheckboxActionViewItem } from 'vs/base/browser/ui/checkbox/checkbox'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; -import { IAction, Action } from 'vs/base/common/actions'; -import { ActionBar, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +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 { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 6c9590bd1e1..02f464009af 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -437,7 +437,13 @@ font-family: var(--monaco-monospace-font); } -.settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-enumDescription { +.settings-editor > .settings-body > .settings-list-container .setting-item-contents .setting-item-markdown hr { + border-bottom-width: 0px; + margin: 10px 20px; + opacity: 0.4; +} + +.settings-editor > .settings-body > .settings-list-container .setting-item-contents .setting-item-enumDescription { display: none; } diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index da898adea82..eb525604877 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ContextSubMenu } from 'vs/base/browser/contextmenu'; import { EventHelper, getDomNodePagePosition } from 'vs/base/browser/dom'; -import { IAction } from 'vs/base/common/actions'; +import { IAction, SubmenuAction } from 'vs/base/common/actions'; import { Delayer } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; @@ -18,8 +17,8 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import { IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import * as nls from 'vs/nls'; -import { ConfigurationTarget, IConfigurationService, overrideIdentifierFromKey } from 'vs/platform/configuration/common/configuration'; -import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationPropertySchema, IConfigurationRegistry, IConfigurationNode, OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry'; +import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationPropertySchema, IConfigurationRegistry, IConfigurationNode, OVERRIDE_PROPERTY_PATTERN, overrideIdentifierFromKey } from 'vs/platform/configuration/common/configurationRegistry'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -826,7 +825,7 @@ class EditSettingRenderer extends Disposable { const anchor = { x: e.event.posx, y: e.event.posy + 10 }; const actions = this.getSettings(editPreferenceWidget.getLine()).length === 1 ? this.getActions(editPreferenceWidget.preferences[0], this.getConfigurationsMap()[editPreferenceWidget.preferences[0].key]) - : editPreferenceWidget.preferences.map(setting => new ContextSubMenu(setting.key, this.getActions(setting, this.getConfigurationsMap()[setting.key]))); + : editPreferenceWidget.preferences.map(setting => new SubmenuAction(`preferences.submenu.${setting.key}`, setting.key, this.getActions(setting, this.getConfigurationsMap()[setting.key]))); this.contextMenuService.showContextMenu({ getAnchor: () => anchor, getActions: () => actions diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index d0f5ac5841e..592600feba1 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -5,7 +5,7 @@ import * as DOM from 'vs/base/browser/dom'; import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { ActionBar, ActionsOrientation, BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { IInputOptions, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { Widget } from 'vs/base/browser/ui/widget'; import { Action, IAction } from 'vs/base/common/actions'; @@ -36,6 +36,7 @@ import { ISettingsGroup, IPreferencesService } from 'vs/workbench/services/prefe import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { isEqual } from 'vs/base/common/resources'; import { registerIcon, Codicon } from 'vs/base/common/codicons'; +import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; export class SettingsHeaderWidget extends Widget implements IViewZone { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 9d24793b79c..a31a0fa0f9f 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -32,14 +32,13 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IProductService } from 'vs/platform/product/common/productService'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { badgeBackground, badgeForeground, contrastBorder, editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { attachButtonStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; -import { getUserDataSyncStore, IUserDataAutoSyncService, IUserDataSyncService, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync'; +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 { attachSuggestEnabledInputBoxStyler, SuggestEnabledInput } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput'; @@ -54,6 +53,7 @@ import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor import { IPreferencesService, ISearchResult, ISettingsEditorModel, ISettingsEditorOptions, SettingsEditorOptions, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { Settings2EditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; +import { IUserDataSyncWorkbenchService } from 'vs/workbench/services/userDataSync/common/userDataSync'; function createGroupIterator(group: SettingsTreeGroupElement): Iterable> { return Iterable.map(group.children, g => { @@ -167,7 +167,7 @@ export class SettingsEditor2 extends BaseEditor { @IEditorGroupsService protected editorGroupService: IEditorGroupsService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, - @IProductService private readonly productService: IProductService, + @IUserDataSyncWorkbenchService private readonly userDataSyncWorkbenchService: IUserDataSyncWorkbenchService, @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService ) { super(SettingsEditor2.ID, telemetryService, themeService, storageService); @@ -472,7 +472,7 @@ export class SettingsEditor2 extends BaseEditor { this.settingsTargetsWidget.settingsTarget = ConfigurationTarget.USER_LOCAL; this.settingsTargetsWidget.onDidTargetChange(target => this.onDidSettingsTargetChange(target)); - if (syncAllowed(this.productService, this.configurationService) && this.userDataAutoSyncService.canToggleEnablement()) { + if (this.userDataSyncWorkbenchService.enabled && this.userDataAutoSyncService.canToggleEnablement()) { const syncControls = this._register(this.instantiationService.createInstance(SyncControls, headerControlsContainer)); this._register(syncControls.onDidChangeLastSyncedLabel(lastSyncedLabel => this.updateInputAriaLabel(lastSyncedLabel))); } @@ -1421,7 +1421,7 @@ class SyncControls extends Disposable { DOM.hide(this.lastSyncedLabel); this.turnOnSyncButton.enabled = true; - this.turnOnSyncButton.label = localize('turnOnSyncButton', "Turn on Preferences Sync"); + this.turnOnSyncButton.label = localize('turnOnSyncButton', "Turn on Settings Sync"); DOM.hide(this.turnOnSyncButton.element); this._register(this.turnOnSyncButton.onDidClick(async () => { @@ -1465,7 +1465,7 @@ class SyncControls extends Disposable { return; } - if (this.userDataAutoSyncService.isEnabled()) { + if (this.userDataAutoSyncService.isEnabled() || this.userDataSyncService.status !== SyncStatus.Idle) { DOM.show(this.lastSyncedLabel); DOM.hide(this.turnOnSyncButton.element); } else { @@ -1479,7 +1479,3 @@ interface ISettingsEditor2State { searchQuery: string; target: SettingsTarget; } - -function syncAllowed(productService: IProductService, configService: IConfigurationService): boolean { - return !!getUserDataSyncStore(productService, configService); -} diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index b66d3b8ad38..ed6f43afcfa 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -217,9 +217,9 @@ export const tocData: ITOCEntry = { settings: ['telemetry.*'] }, { - id: 'application/sync', - label: localize('sync', "Sync"), - settings: ['sync.*'] + id: 'application/settingsSync', + label: localize('settingsSync', "Settings Sync"), + settings: ['settingsSync.*'] } ] } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 03340c7ded9..c30a7ee401b 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -8,7 +8,6 @@ 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 { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { alert as ariaAlert } from 'vs/base/browser/ui/aria/aria'; import { Button } from 'vs/base/browser/ui/button/button'; import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; @@ -20,7 +19,7 @@ import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IObjectTreeOptions, ObjectTree } 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 } from 'vs/base/common/actions'; +import { Action, IAction, Separator } from 'vs/base/common/actions'; import * as arrays from 'vs/base/common/arrays'; import { Color, RGBA } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -500,7 +499,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre this.ignoredSettings = getIgnoredSettings(getDefaultIgnoredSettings(), this._configService); this._register(this._configService.onDidChangeConfiguration(e => { - if (e.affectedKeys.includes('sync.ignoredSettings')) { + if (e.affectedKeys.includes('settingsSync.ignoredSettings')) { this.ignoredSettings = getIgnoredSettings(getDefaultIgnoredSettings(), this._configService); this._onDidChangeIgnoredSettings.fire(); } @@ -605,7 +604,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre } private fixToolbarIcon(toolbar: ToolBar): void { - const button = toolbar.getContainer().querySelector('.codicon-toolbar-more'); + const button = toolbar.getElement().querySelector('.codicon-toolbar-more'); if (button) { (button).tabIndex = -1; @@ -1072,7 +1071,7 @@ export class SettingObjectRenderer extends AbstractSettingRenderer implements IT : {}; const newValue: Record = {}; - let newItems: IObjectDataItem[] = []; + const newItems: IObjectDataItem[] = []; template.objectWidget.items.forEach((item, idx) => { // Item was updated @@ -2026,7 +2025,7 @@ class SyncSettingAction extends Action { @IConfigurationService private readonly configService: IConfigurationService, ) { super(SyncSettingAction.ID, SyncSettingAction.LABEL); - this._register(Event.filter(configService.onDidChangeConfiguration, e => e.affectsConfiguration('sync.ignoredSettings'))(() => this.update())); + this._register(Event.filter(configService.onDidChangeConfiguration, e => e.affectsConfiguration('settingsSync.ignoredSettings'))(() => this.update())); this.update(); } @@ -2037,7 +2036,7 @@ class SyncSettingAction extends Action { async run(): Promise { // first remove the current setting completely from ignored settings - let currentValue = [...this.configService.getValue('sync.ignoredSettings')]; + let currentValue = [...this.configService.getValue('settingsSync.ignoredSettings')]; currentValue = currentValue.filter(v => v !== this.setting.key && v !== `-${this.setting.key}`); const defaultIgnoredSettings = getDefaultIgnoredSettings(); @@ -2054,7 +2053,7 @@ class SyncSettingAction extends Action { currentValue.push(this.setting.key); } - this.configService.updateValue('sync.ignoredSettings', currentValue.length ? currentValue : undefined, ConfigurationTarget.USER); + this.configService.updateValue('settingsSync.ignoredSettings', currentValue.length ? currentValue : undefined, ConfigurationTarget.USER); return Promise.resolve(undefined); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index 63a4da5c0aa..f68d3c6d14a 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -424,7 +424,7 @@ function wordifyKey(key: string): string { match; }); - for (let [k, v] of knownTermMappings) { + for (const [k, v] of knownTermMappings) { key = key.replace(new RegExp(`\\b${k}\\b`, 'gi'), v); } @@ -631,7 +631,7 @@ const tagRegex = /(^|\s)@tag:("([^"]*)"|[^"]\S*)/g; const extensionRegex = /(^|\s)@ext:("([^"]*)"|[^"]\S*)?/g; export function parseQuery(query: string): IParsedQuery { const tags: string[] = []; - let extensions: string[] = []; + const extensions: string[] = []; query = query.replace(tagRegex, (_, __, quotedTag, tag) => { tags.push(tag || quotedTag); return ''; @@ -643,7 +643,7 @@ export function parseQuery(query: string): IParsedQuery { }); query = query.replace(extensionRegex, (_, __, quotedExtensionId, extensionId) => { - let extensionIdQuery: string = extensionId || quotedExtensionId; + const extensionIdQuery: string = extensionId || quotedExtensionId; if (extensionIdQuery) { extensions.push(...extensionIdQuery.split(',').map(s => s.trim()).filter(s => !isFalsyOrWhitespace(s))); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index 35ef37de64e..d7f85b56922 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -780,6 +780,7 @@ export class ObjectSettingWidget extends AbstractListSettingWidget { changedItem.key = key; + okButton.enabled = key.data !== ''; const suggestedValue = this.valueSuggester(key.data) ?? item.value; @@ -843,6 +844,7 @@ export class ObjectSettingWidget extends AbstractListSettingWidget(JSONContributionRegistry.Extensions.JSONContribution); @@ -153,6 +154,7 @@ export class PreferencesContribution implements IWorkbenchContribution { const registry = Registry.as(Extensions.Configuration); registry.registerConfiguration({ + ...workbenchConfigurationNodeBase, 'properties': { 'workbench.settings.enableNaturalLanguageSearch': { 'type': 'boolean', diff --git a/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts b/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts index abde9a4b078..d8021dd0c07 100644 --- a/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts +++ b/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts @@ -7,7 +7,6 @@ import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { IAction, Action } from 'vs/base/common/actions'; -import { SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; @@ -19,6 +18,7 @@ import { isStringArray } from 'vs/base/common/types'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; export interface IRemoteSelectItem extends ISelectOptionItem { authority: string[]; diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 7a73c327e9a..930d4e799cc 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -21,11 +21,11 @@ import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent, ITreeMouseEvent } from 'vs/base/browser/ui/tree/tree'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { Disposable, IDisposable, toDisposable, MutableDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; -import { ActionBar, ActionViewItem, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { ActionRunner, IAction } from 'vs/base/common/actions'; -import { IMenuService, MenuId, IMenu, MenuRegistry, MenuItemAction, ILocalizedString } from 'vs/platform/actions/common/actions'; -import { createAndFillInContextMenuActions, createAndFillInActionBarActions, ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, MenuId, IMenu, MenuRegistry, MenuItemAction, ILocalizedString, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { createAndFillInContextMenuActions, createAndFillInActionBarActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IRemoteExplorerService, TunnelModel, MakeAddress, TunnelType, ITunnelItem, Tunnel } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -41,6 +41,7 @@ import { 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'; export const forwardedPortsViewEnabled = new RawContextKey('forwardedPortsViewEnabled', false); @@ -157,8 +158,18 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { get candidates(): TunnelItem[] { const candidates: TunnelItem[] = []; this._candidates.forEach(value => { - const key = MakeAddress(value.host, value.port); + let key = MakeAddress(value.host, value.port); if (!this.model.forwarded.has(key) && !this.model.detected.has(key)) { + // 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)) { + return; + } + } candidates.push(new TunnelItem(TunnelType.Candidate, value.host, value.port, undefined, undefined, false, undefined, value.detail)); } }); @@ -213,10 +224,11 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer { if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(ContextAwareMenuEntryActionViewItem, action); + return this.instantiationService.createInstance(MenuEntryActionViewItem, action); + } else if (action instanceof SubmenuItemAction) { + return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); } return undefined; @@ -451,7 +463,6 @@ export class TunnelPanel extends ViewPane { @IQuickInputService protected quickInputService: IQuickInputService, @ICommandService protected commandService: ICommandService, @IMenuService private readonly menuService: IMenuService, - @INotificationService private readonly notificationService: INotificationService, @IContextViewService private readonly contextViewService: IContextViewService, @IThemeService themeService: IThemeService, @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, @@ -647,10 +658,6 @@ export class TunnelPanel extends ViewPane { super.layoutBody(height, width); this.tree.layout(height, width); } - - getActionViewItem(action: IAction): IActionViewItem | undefined { - return action instanceof MenuItemAction ? new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService) : undefined; - } } export class TunnelPanelDescriptor implements IViewDescriptor { diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index dbd34cdc3d8..fd8ee7ee9a2 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -97,11 +97,13 @@ const extensionKindSchema: IJSONSchema = { type: 'string', enum: [ 'ui', - 'workspace' + 'workspace', + 'web' ], enumDescriptions: [ localize('ui', "UI extension kind. In a remote window, such extensions are enabled only when available on the local machine."), - localize('workspace', "Workspace extension kind. In a remote window, such extensions are enabled only when available on the remote.") + localize('workspace', "Workspace extension kind. In a remote window, such extensions are enabled only when available on the remote."), + localize('web', "Web worker extension kind. Such an extension can execute in a web worker extension host.") ], }; diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index b5b9adcdb5d..c2ef67dfb97 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -42,6 +42,7 @@ export class SCMStatusController implements IWorkbenchContribution { ) { this.focusedProviderContextKey = contextKeyService.createKey('scmProvider', undefined); this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); + this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); const onDidChangeSCMCountBadge = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.countBadge')); onDidChangeSCMCountBadge(this.renderActivityCount, this, this.disposables); @@ -123,14 +124,22 @@ export class SCMStatusController implements IWorkbenchContribution { this.focusRepository(repository); } + private onDidRemoveRepository(repository: ISCMRepository): void { + if (this.focusedRepository !== repository) { + return; + } + + this.focusRepository(this.scmService.repositories[0]); + } + private focusRepository(repository: ISCMRepository | undefined): void { if (this.focusedRepository === repository) { return; } + this.focusDisposable.dispose(); this.focusedRepository = repository; this.focusedProviderContextKey.set(repository && repository.provider.id); - this.focusDisposable.dispose(); if (repository && repository.provider.onDidChangeStatusBarCommands) { this.focusDisposable = repository.provider.onDidChangeStatusBarCommands(() => this.renderStatusBar(repository)); diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 0b734d2927d..450448112bb 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -34,19 +34,17 @@ import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/co import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { IDiffEditorOptions, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Action, IAction, ActionRunner } from 'vs/base/common/actions'; -import { IActionBarOptions, ActionsOrientation, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IActionBarOptions, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { basename, isEqualOrParent } from 'vs/base/common/resources'; import { MenuId, IMenuService, IMenu, MenuItemAction, MenuRegistry } from 'vs/platform/actions/common/actions'; -import { createAndFillInActionBarActions, ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IChange, IEditorModel, ScrollType, IEditorContribution, IDiffEditorModel } from 'vs/editor/common/editorCommon'; import { OverviewRulerLane, ITextModel, IModelDecorationOptions, MinimapPosition } from 'vs/editor/common/model'; import { sortedDiff, firstIndex } from 'vs/base/common/arrays'; import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ISplice } from 'vs/base/common/sequence'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { INotificationService } from 'vs/platform/notification/common/notification'; import { createStyleSheet } from 'vs/base/browser/dom'; import { ITextFileEditorModel, IResolvedTextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { EncodingMode } from 'vs/workbench/common/editor'; @@ -176,14 +174,11 @@ class DirtyDiffWidget extends PeekViewWidget { editor: ICodeEditor, private model: DirtyDiffModel, @IThemeService private readonly themeService: IThemeService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IMenuService menuService: IMenuService, - @IKeybindingService private readonly keybindingService: IKeybindingService, - @INotificationService private readonly notificationService: INotificationService, - @IContextKeyService contextKeyService: IContextKeyService, - @IContextMenuService private readonly contextMenuService: IContextMenuService + @IContextKeyService contextKeyService: IContextKeyService ) { - super(editor, { isResizeable: true, frameWidth: 1, keepEditorSelection: true }); + super(editor, { isResizeable: true, frameWidth: 1, keepEditorSelection: true }, instantiationService); this._disposables.add(themeService.onDidColorThemeChange(this._applyTheme, this)); this._applyTheme(themeService.getColorTheme()); @@ -274,20 +269,12 @@ class DirtyDiffWidget extends PeekViewWidget { }); return { + ...super._getActionBarOptions(), actionRunner, - actionViewItemProvider: action => this.getActionViewItem(action), orientation: ActionsOrientation.HORIZONTAL_REVERSE }; } - getActionViewItem(action: IAction): IActionViewItem | undefined { - if (!(action instanceof MenuItemAction)) { - return undefined; - } - - return new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); - } - protected _fillBody(container: HTMLElement): void { const options: IDiffEditorOptions = { scrollBeyondLastLine: true, diff --git a/src/vs/workbench/contrib/scm/browser/menus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts index 6f1eebbe641..500f7fa585a 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -4,219 +4,238 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/scm'; -import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable, Disposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; +import { Emitter } from 'vs/base/common/event'; +import { IDisposable, Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; -import { IAction, Action } from 'vs/base/common/actions'; -import { createAndFillInContextMenuActions, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +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 { isSCMResource } from './util'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { equals } from 'vs/base/common/arrays'; import { ISplice, ISequence } from 'vs/base/common/sequence'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { localize } from 'vs/nls'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; function actionEquals(a: IAction, b: IAction): boolean { return a.id === b.id; } -interface ISCMResourceGroupMenuEntry { - readonly group: ISCMResourceGroup; - readonly disposable: IDisposable; -} +export class SCMTitleMenu { -interface ISCMMenus { - readonly resourceGroupMenu: IMenu; - readonly resourceMenu: IMenu; - readonly resourceFolderMenu: IMenu; -} + private _actions: IAction[] = []; + get actions(): IAction[] { return this._actions; } -export function getSCMResourceContextKey(resource: ISCMResourceGroup | ISCMResource): string { - return isSCMResource(resource) ? resource.resourceGroup.id : resource.id; -} - -export class SCMRepositoryMenus implements IDisposable { - - private contextKeyService: IContextKeyService; - - readonly titleMenu: IMenu; - private titleActionDisposable: IDisposable = Disposable.None; - private titleActions: IAction[] = []; - private titleSecondaryActions: IAction[] = []; + private _secondaryActions: IAction[] = []; + get secondaryActions(): IAction[] { return this._secondaryActions; } private readonly _onDidChangeTitle = new Emitter(); - readonly onDidChangeTitle: Event = this._onDidChangeTitle.event; + readonly onDidChangeTitle = this._onDidChangeTitle.event; - private readonly resourceGroupMenuEntries: ISCMResourceGroupMenuEntry[] = []; - private readonly resourceGroupMenus = new Map(); - - private readonly disposables = new DisposableStore(); + readonly menu: IMenu; + private listener: IDisposable = Disposable.None; + private disposables = new DisposableStore(); constructor( - readonly provider: ISCMProvider | undefined, - @IContextKeyService contextKeyService: IContextKeyService, - @ICommandService private readonly commandService: ICommandService, - @IMenuService private readonly menuService: IMenuService, - @IContextMenuService private readonly contextMenuService: IContextMenuService + @IMenuService menuService: IMenuService, + @IContextKeyService contextKeyService: IContextKeyService ) { - this.contextKeyService = contextKeyService.createScoped(); - const scmProviderKey = this.contextKeyService.createKey('scmProvider', undefined); + this.menu = menuService.createMenu(MenuId.SCMTitle, contextKeyService); + this.disposables.add(this.menu); - if (provider) { - scmProviderKey.set(provider.contextValue); - provider.groups.onDidSplice(this.onDidSpliceGroups, this, this.disposables); - this.onDidSpliceGroups({ start: 0, deleteCount: 0, toInsert: provider.groups.elements }); - } else { - scmProviderKey.set(''); - } - - this.titleMenu = this.menuService.createMenu(MenuId.SCMTitle, this.contextKeyService); - this.disposables.add(this.titleMenu); - this.titleMenu.onDidChange(this.updateTitleActions, this, this.disposables); + this.menu.onDidChange(this.updateTitleActions, this, this.disposables); this.updateTitleActions(); } private updateTitleActions(): void { const primary: IAction[] = []; const secondary: IAction[] = []; + const disposable = createAndFillInActionBarActions(this.menu, { shouldForwardArgs: true }, { primary, secondary }); - const disposable = createAndFillInActionBarActions(this.titleMenu, { shouldForwardArgs: true }, { primary, secondary }); - - if (equals(primary, this.titleActions, actionEquals) && equals(secondary, this.titleSecondaryActions, actionEquals)) { + if (equals(primary, this._actions, actionEquals) && equals(secondary, this._secondaryActions, actionEquals)) { disposable.dispose(); return; } - this.titleActionDisposable.dispose(); - this.titleActionDisposable = disposable; - this.titleActions = primary; - this.titleSecondaryActions = secondary; + this.listener.dispose(); + this.listener = disposable; + this._actions = primary; + this._secondaryActions = secondary; this._onDidChangeTitle.fire(); } - getTitleActions(): IAction[] { - return this.titleActions; + dispose(): void { + this.menu.dispose(); + this.listener.dispose(); } +} - getTitleSecondaryActions(): IAction[] { - return this.titleSecondaryActions; - } +interface IContextualResourceMenuItem { + readonly menu: IMenu; + dispose(): void; +} - getRepositoryContextActions(): IAction[] { - if (!this.provider) { - return []; +class SCMMenusItem { + + private _resourceGroupMenu: IMenu | undefined; + get resourceGroupMenu(): IMenu { + if (!this._resourceGroupMenu) { + this._resourceGroupMenu = this.menuService.createMenu(MenuId.SCMResourceGroupContext, this.contextKeyService); } - const contextKeyService = this.contextKeyService.createScoped(); - const scmProviderKey = contextKeyService.createKey('scmProvider', undefined); - scmProviderKey.set(this.provider.contextValue); + return this._resourceGroupMenu; + } - const menu = this.menuService.createMenu(MenuId.SCMSourceControl, contextKeyService); - const primary: IAction[] = []; - const secondary: IAction[] = []; - const result = { primary, secondary }; - const disposable = createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => g === 'inline'); - - disposable.dispose(); - menu.dispose(); - contextKeyService.dispose(); - - if (this.provider.rootUri) { - secondary.push(new Action('_openInTerminal', localize('open in terminal', "Open In Terminal"), undefined, true, async () => { - await this.commandService.executeCommand('openInTerminal', this.provider!.rootUri); - })); + private _resourceFolderMenu: IMenu | undefined; + get resourceFolderMenu(): IMenu { + if (!this._resourceFolderMenu) { + this._resourceFolderMenu = this.menuService.createMenu(MenuId.SCMResourceFolderContext, this.contextKeyService); } - return secondary; + return this._resourceFolderMenu; } - getResourceGroupContextActions(group: ISCMResourceGroup): IAction[] { - return this.getActions(MenuId.SCMResourceGroupContext, group).secondary; + private genericResourceMenu: IMenu | undefined; + private contextualResourceMenus: Map | undefined; + + constructor( + private contextKeyService: IContextKeyService, + private menuService: IMenuService + ) { } + + getResourceMenu(resource: ISCMResource): IMenu { + if (typeof resource.contextValue === 'undefined') { + if (!this.genericResourceMenu) { + this.genericResourceMenu = this.menuService.createMenu(MenuId.SCMResourceContext, this.contextKeyService); + } + + return this.genericResourceMenu; + } + + if (!this.contextualResourceMenus) { + this.contextualResourceMenus = new Map(); + } + + let item = this.contextualResourceMenus.get(resource.contextValue); + + if (!item) { + const contextKeyService = this.contextKeyService.createScoped(); + contextKeyService.createKey('scmResourceState', resource.contextValue); + + const menu = this.menuService.createMenu(MenuId.SCMResourceContext, contextKeyService); + + item = { + menu, dispose() { + menu.dispose(); + contextKeyService.dispose(); + } + }; + + this.contextualResourceMenus.set(resource.contextValue, item); + } + + return item.menu; } - getResourceContextActions(resource: ISCMResource): IAction[] { - return this.getActions(MenuId.SCMResourceContext, resource).secondary; + dispose(): void { + this.resourceGroupMenu?.dispose(); + this.genericResourceMenu?.dispose(); + + if (this.contextualResourceMenus) { + dispose(this.contextualResourceMenus.values()); + this.contextualResourceMenus.clear(); + this.contextualResourceMenus = undefined; + } + + this.resourceFolderMenu?.dispose(); + this.contextKeyService.dispose(); + } +} + +export class SCMRepositoryMenus implements IDisposable { + + private contextKeyService: IContextKeyService; + + readonly titleMenu: SCMTitleMenu; + private repositoryMenu: IMenu | undefined; + private readonly resourceGroups: ISCMResourceGroup[] = []; + private readonly resourceGroupMenusItems = new Map(); + + private readonly disposables = new DisposableStore(); + + constructor( + readonly 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('scmProviderHasRootUri', !!provider.rootUri); + + const serviceCollection = new ServiceCollection([IContextKeyService, this.contextKeyService]); + instantiationService = instantiationService.createChild(serviceCollection); + this.titleMenu = instantiationService.createInstance(SCMTitleMenu); + + provider.groups.onDidSplice(this.onDidSpliceGroups, this, this.disposables); + this.onDidSpliceGroups({ start: 0, deleteCount: 0, toInsert: provider.groups.elements }); } - getResourceFolderContextActions(group: ISCMResourceGroup): IAction[] { - return this.getActions(MenuId.SCMResourceFolderContext, group).secondary; + getRepositoryMenu(): IMenu { + if (!this.repositoryMenu) { + this.repositoryMenu = this.menuService.createMenu(MenuId.SCMSourceControl, this.contextKeyService); + this.disposables.add(this.repositoryMenu); + } + + return this.repositoryMenu; } - private getActions(menuId: MenuId, resource: ISCMResourceGroup | ISCMResource): { primary: IAction[]; secondary: IAction[]; } { - const contextKeyService = this.contextKeyService.createScoped(); - contextKeyService.createKey('scmResourceGroup', getSCMResourceContextKey(resource)); + getResourceGroupMenu(group: ISCMResourceGroup): IMenu { + return this.getOrCreateResourceGroupMenusItem(group).resourceGroupMenu; + } - const menu = this.menuService.createMenu(menuId, contextKeyService); - const primary: IAction[] = []; - const secondary: IAction[] = []; - const result = { primary, secondary }; - createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => /^inline/.test(g)); + getResourceMenu(resource: ISCMResource): IMenu { + return this.getOrCreateResourceGroupMenusItem(resource.resourceGroup).getResourceMenu(resource); + } - menu.dispose(); - contextKeyService.dispose(); + getResourceFolderMenu(group: ISCMResourceGroup): IMenu { + return this.getOrCreateResourceGroupMenusItem(group).resourceFolderMenu; + } + + private getOrCreateResourceGroupMenusItem(group: ISCMResourceGroup): SCMMenusItem { + let result = this.resourceGroupMenusItems.get(group); + + if (!result) { + const contextKeyService = this.contextKeyService.createScoped(); + contextKeyService.createKey('scmProvider', group.provider.contextValue); + contextKeyService.createKey('scmResourceGroup', group.id); + + result = new SCMMenusItem(contextKeyService, this.menuService); + this.resourceGroupMenusItems.set(group, result); + } return result; } - getResourceGroupMenu(group: ISCMResourceGroup): IMenu { - if (!this.resourceGroupMenus.has(group)) { - throw new Error('SCM Resource Group menu not found'); - } - - return this.resourceGroupMenus.get(group)!.resourceGroupMenu; - } - - getResourceMenu(group: ISCMResourceGroup): IMenu { - if (!this.resourceGroupMenus.has(group)) { - throw new Error('SCM Resource Group menu not found'); - } - - return this.resourceGroupMenus.get(group)!.resourceMenu; - } - - getResourceFolderMenu(group: ISCMResourceGroup): IMenu { - if (!this.resourceGroupMenus.has(group)) { - throw new Error('SCM Resource Group menu not found'); - } - - return this.resourceGroupMenus.get(group)!.resourceFolderMenu; - } - private onDidSpliceGroups({ start, deleteCount, toInsert }: ISplice): void { - const menuEntriesToInsert = toInsert.map(group => { - const contextKeyService = this.contextKeyService.createScoped(); - contextKeyService.createKey('scmProvider', group.provider.contextValue); - contextKeyService.createKey('scmResourceGroup', getSCMResourceContextKey(group)); + const deleted = this.resourceGroups.splice(start, deleteCount, ...toInsert); - const resourceGroupMenu = this.menuService.createMenu(MenuId.SCMResourceGroupContext, contextKeyService); - const resourceMenu = this.menuService.createMenu(MenuId.SCMResourceContext, contextKeyService); - const resourceFolderMenu = this.menuService.createMenu(MenuId.SCMResourceFolderContext, contextKeyService); - const disposable = combinedDisposable(contextKeyService, resourceGroupMenu, resourceMenu, resourceFolderMenu); - - this.resourceGroupMenus.set(group, { resourceGroupMenu, resourceMenu, resourceFolderMenu }); - return { group, disposable }; - }); - - const deleted = this.resourceGroupMenuEntries.splice(start, deleteCount, ...menuEntriesToInsert); - - for (const entry of deleted) { - this.resourceGroupMenus.delete(entry.group); - entry.disposable.dispose(); + for (const group of deleted) { + const item = this.resourceGroupMenusItems.get(group); + item?.dispose(); + this.resourceGroupMenusItems.delete(group); } } dispose(): void { this.disposables.dispose(); - this.resourceGroupMenuEntries.forEach(e => e.disposable.dispose()); + this.resourceGroupMenusItems.forEach(item => item.dispose()); } } export class SCMMenus { + readonly titleMenu: SCMTitleMenu; private readonly disposables = new DisposableStore(); private readonly entries: { repository: ISCMRepository, dispose: () => void }[] = []; private readonly menus = new Map(); @@ -225,6 +244,8 @@ export class SCMMenus { repositories: ISequence, @IInstantiationService private instantiationService: IInstantiationService ) { + this.titleMenu = instantiationService.createInstance(SCMTitleMenu); + repositories.onDidSplice(this.onDidSplice, this, this.disposables); this.onDidSplice({ start: 0, deleteCount: 0, toInsert: repositories.elements }); } diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index a05fc7e0011..b0cda5d05ad 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -7,14 +7,14 @@ 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 } from 'vs/workbench/contrib/scm/common/scm'; +import { VIEWLET_ID, ISCMRepository, ISCMService, VIEW_PANE_ID, ISCMProvider } 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'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { SCMService } from 'vs/workbench/contrib/scm/common/scmService'; @@ -74,14 +74,10 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ win: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G }, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G }, mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_G }, - handler: accessor => { + handler: async accessor => { const viewsService = accessor.get(IViewsService); - - if (viewsService.isViewVisible(VIEW_PANE_ID)) { - viewsService.closeView(VIEW_PANE_ID); - } else { - viewsService.openView(VIEW_PANE_ID); - } + const view = await viewsService.openView(VIEW_PANE_ID); + view?.focus(); } }); @@ -167,6 +163,11 @@ Registry.as(ConfigurationExtensions.Configuration).regis type: 'string', markdownDescription: localize('inputFontFamily', "Controls the font for the input message. Use `default` for the workbench user interface font family, `editor` for the `#editor.fontFamily#`'s value, or a custom font family."), default: 'default' + }, + 'scm.alwaysShowRepositories': { + type: 'boolean', + markdownDescription: localize('alwaysShowRepository', "Controls whether repositories should always be visible in the SCM view."), + default: false } } }); @@ -205,4 +206,22 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +CommandsRegistry.registerCommand('scm.openInTerminal', async (accessor, provider: ISCMProvider) => { + if (!provider || !provider.rootUri) { + return; + } + + const commandService = accessor.get(ICommandService); + await commandService.executeCommand('openInTerminal', provider.rootUri); +}); + +MenuRegistry.appendMenuItem(MenuId.SCMSourceControl, { + group: '100_end', + command: { + id: 'scm.openInTerminal', + title: localize('open in terminal', "Open In Terminal") + }, + when: ContextKeyExpr.equals('scmProviderHasRootUri', true) +}); + registerSingleton(ISCMService, SCMService); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 2f4155b9563..faf74c03613 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -20,17 +20,15 @@ import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/c 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 } from 'vs/base/common/actions'; -import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { SCMMenus } from './menus'; -import { ActionBar, IActionViewItemProvider, Separator, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +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 } from './util'; +import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, connectPrimaryMenu, collectContextMenuActions } from './util'; import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; import { WorkbenchCompressibleObjectTree, IOpenEvent } from 'vs/platform/list/browser/listService'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService, ConfigurationTarget, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { disposableTimeout, ThrottledDelayer } from 'vs/base/common/async'; -import { INotificationService } from 'vs/platform/notification/common/notification'; 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'; @@ -72,15 +70,13 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IModeService } from 'vs/editor/common/services/modeService'; import { ILabelService } from 'vs/platform/label/common/label'; -import { ContextSubMenu } from 'vs/base/browser/contextmenu'; 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 { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; -import { domEvent } from 'vs/base/browser/event'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; type TreeElement = ISCMRepository | ISCMInput | ISCMResourceGroup | IResourceNode | ISCMResource; @@ -232,7 +228,7 @@ class RepositoryRenderer implements ICompressibleTreeRenderer { + disposables.add(connectPrimaryMenu(menus.titleMenu.menu, (primary, secondary) => { menuPrimaryActions = primary; menuSecondaryActions = secondary; updateToolbar(); @@ -275,7 +271,6 @@ class InputRenderer implements ICompressibleTreeRenderer void, - private focusTree: () => void, @IInstantiationService private instantiationService: IInstantiationService, ) { } @@ -288,10 +283,6 @@ class InputRenderer implements ICompressibleTreeRenderer new StandardKeyboardEvent(e)); - const onEscape = Event.filter(onKeyDown, e => e.keyCode === KeyCode.Escape); - disposables.add(onEscape(this.focusTree)); - return { inputWidget, disposable: Disposable.None, templateDisposable: disposables }; } @@ -301,7 +292,6 @@ class InputRenderer implements ICompressibleTreeRenderer templateData.inputWidget.input = undefined }); // Remember widget this.inputWidgets.set(input, templateData.inputWidget); @@ -356,6 +346,22 @@ class InputRenderer implements ICompressibleTreeRenderer { + const theme = this.themeService.getColorTheme(); + const icon = iconResource && (theme.type === LIGHT ? iconResource.decorations.icon : iconResource.decorations.iconDark); - if (icon) { - template.decorationIcon.style.display = ''; - template.decorationIcon.style.backgroundImage = `url('${icon}')`; - template.decorationIcon.title = tooltip; - } else { - template.decorationIcon.style.display = 'none'; - template.decorationIcon.style.backgroundImage = ''; - template.decorationIcon.title = ''; - } + template.fileLabel.setFile(uri, { + fileDecorations: { colors: false, badges: !icon }, + hidePath: viewModel.mode === ViewModelMode.Tree, + fileKind, + matches, + descriptionMatches + }); + + if (icon) { + template.decorationIcon.style.display = ''; + template.decorationIcon.style.backgroundImage = `url('${icon}')`; + template.decorationIcon.title = tooltip; + } else { + template.decorationIcon.style.display = 'none'; + template.decorationIcon.style.backgroundImage = ''; + template.decorationIcon.title = ''; + } + }; + + elementDisposables.add(this.themeService.onDidColorThemeChange(render)); + render(); template.element.setAttribute('data-tooltip', tooltip); template.elementDisposables = elementDisposables; @@ -865,8 +875,10 @@ class ViewModel { private items: IRepositoryItem[] = []; private visibilityDisposables = new DisposableStore(); private scrollTop: number | undefined; + private alwaysShowRepositories = false; private firstVisible = true; private repositoryCollapseStates: Map | undefined; + private viewSubMenuAction: SCMViewSubMenuAction | undefined; private disposables = new DisposableStore(); constructor( @@ -883,6 +895,16 @@ class ViewModel { this._onDidChangeRepositoryCollapseState.event, Event.signal(Event.filter(this.tree.onDidChangeCollapseState, e => isSCMRepository(e.node.element))) ); + + configurationService.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); + this.onDidChangeConfiguration(); + } + + private onDidChangeConfiguration(e?: IConfigurationChangeEvent): void { + if (!e || e.affectsConfiguration('scm.alwaysShowRepositories')) { + this.alwaysShowRepositories = this.configurationService.getValue('scm.alwaysShowRepositories'); + this.refresh(); + } } private _onDidSpliceRepositories({ start, deleteCount, toInsert }: ISplice): void { @@ -995,7 +1017,9 @@ class ViewModel { } private refresh(item?: IRepositoryItem | IGroupItem): void { - if (this.items.length === 1 && (!item || isRepositoryItem(item))) { + 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); } else if (item) { this.tree.setChildren(item.element, this.render(item).children); @@ -1003,6 +1027,14 @@ class ViewModel { this.tree.setChildren(null, this.items.map(item => this.render(item))); } + if (focusedInput) { + const inputWidget = this.inputRenderer.getRenderedInputWidget(focusedInput); + + if (inputWidget) { + inputWidget.focus(); + } + } + this._onDidChangeRepositoryCollapseState.fire(); } @@ -1086,37 +1118,48 @@ class ViewModel { } getViewActions(): IAction[] { - if (this.repositories.elements.length !== 1) { + if (this.repositories.elements.length === 0) { + return this.menus.titleMenu.actions; + } + + if (this.alwaysShowRepositories || this.repositories.elements.length !== 1) { return []; } const menus = this.menus.getRepositoryMenus(this.repositories.elements[0].provider); - return menus.getTitleActions(); + return menus.titleMenu.actions; } getViewSecondaryActions(): IAction[] { if (this.repositories.elements.length === 0) { - return []; + return this.menus.titleMenu.secondaryActions; } - const viewAction = new SCMViewSubMenuAction(this); + if (!this.viewSubMenuAction) { + this.viewSubMenuAction = new SCMViewSubMenuAction(this); + this.disposables.add(this.viewSubMenuAction); + } - if (this.repositories.elements.length !== 1) { - return viewAction.entries; + if (this.alwaysShowRepositories || this.repositories.elements.length !== 1) { + return this.viewSubMenuAction.actions; } const menus = this.menus.getRepositoryMenus(this.repositories.elements[0].provider); - const secondaryActions = menus.getTitleSecondaryActions(); + const secondaryActions = menus.titleMenu.secondaryActions; if (secondaryActions.length === 0) { - return [viewAction]; + return [this.viewSubMenuAction]; } - return [viewAction, new Separator(), ...secondaryActions]; + return [this.viewSubMenuAction, new Separator(), ...secondaryActions]; } getViewActionsContext(): any { - if (this.repositories.elements.length !== 1) { + if (this.repositories.elements.length === 0) { + return []; + } + + if (this.alwaysShowRepositories || this.repositories.elements.length !== 1) { return undefined; } @@ -1167,55 +1210,66 @@ class ViewModel { } } -class SCMViewSubMenuAction extends ContextSubMenu { +class SCMViewSubMenuAction extends SubmenuAction { + + readonly actions!: IAction[]; + constructor(viewModel: ViewModel) { - super(localize('sortAction', "View & Sort"), + 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); + + super( + 'scm.viewsort', + localize('sortAction', "View & Sort"), [ - ...new RadioGroup([ - new SCMViewModeListAction(viewModel), - new SCMViewModeTreeAction(viewModel) - ]).actions, + ...new RadioGroup([listAction, treeAction]).actions, new Separator(), - ...new RadioGroup([ - new SCMSortByNameAction(viewModel), - new SCMSortByPathAction(viewModel), - new SCMSortByStatusAction(viewModel) - ]).actions + ...new RadioGroup([sortByNameAction, sortByPathAction, sortByStatusAction]).actions ] ); + + this._register(combinedDisposable(listAction, treeAction, sortByNameAction, sortByPathAction, sortByStatusAction)); } } -abstract class SCMViewModeAction extends Action { - constructor(id: string, label: string, private viewModel: ViewModel, private viewMode: ViewModelMode) { - super(id, label); +export class ToggleViewModeAction extends Action { - this.checked = this.viewModel.mode === this.viewMode; + static readonly ID = 'workbench.scm.action.toggleViewMode'; + static readonly LABEL = localize('toggleViewMode', "Toggle View Mode"); + + constructor(id: string = ToggleViewModeAction.ID, label: string = ToggleViewModeAction.LABEL, private viewModel: ViewModel, private mode?: ViewModelMode) { + super(id, label); + this._register(this.viewModel.onDidChangeMode(this.onDidChangeMode, this)); + this.onDidChangeMode(this.viewModel.mode); } async run(): Promise { - if (this.viewMode !== this.viewModel.mode) { - this.checked = !this.checked; - this.viewModel.mode = this.viewMode; + if (typeof this.mode === 'undefined') { + this.viewModel.mode = this.viewModel.mode === ViewModelMode.List ? ViewModelMode.Tree : ViewModelMode.List; + } else { + this.viewModel.mode = this.mode; } } -} -class SCMViewModeListAction extends SCMViewModeAction { - static readonly ID = 'workbench.scm.action.viewModeList'; - static readonly LABEL = localize('viewModeList', "View as List"); - - constructor(viewModel: ViewModel) { - super(SCMViewModeListAction.ID, SCMViewModeListAction.LABEL, viewModel, ViewModelMode.List); + private onDidChangeMode(mode: ViewModelMode): void { + const iconClass = mode === ViewModelMode.List ? 'codicon-list-tree' : 'codicon-list-flat'; + this.class = `scm-action toggle-view-mode ${iconClass}`; + this.checked = this.viewModel.mode === this.mode; } } -class SCMViewModeTreeAction extends SCMViewModeAction { - static readonly ID = 'workbench.scm.action.viewModeTree'; - static readonly LABEL = localize('viewModeTree', "View as Tree"); - +class SCMViewModeListAction extends ToggleViewModeAction { constructor(viewModel: ViewModel) { - super(SCMViewModeTreeAction.ID, SCMViewModeTreeAction.LABEL, viewModel, ViewModelMode.Tree); + super('workbench.scm.action.viewModeList', localize('viewModeList', "View as List"), viewModel, ViewModelMode.List); + } +} + +class SCMViewModeTreeAction extends ToggleViewModeAction { + constructor(viewModel: ViewModel) { + super('workbench.scm.action.viewModeTree', localize('viewModeTree', "View as Tree"), viewModel, ViewModelMode.Tree); } } @@ -1294,6 +1348,10 @@ class SCMInputWidget extends Disposable { } set input(input: ISCMInput | undefined) { + if (input === this.input) { + return; + } + this.validationDisposable.dispose(); removeClass(this.editorContainer, 'synthetic-focus'); @@ -1433,7 +1491,8 @@ class SCMInputWidget extends Disposable { wrappingStrategy: 'advanced', wrappingIndent: 'none', padding: { top: 3, bottom: 3 }, - quickSuggestions: false + quickSuggestions: false, + scrollbar: { alwaysConsumeMouseWheel: false } }; const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { @@ -1492,6 +1551,10 @@ class SCMInputWidget extends Disposable { addClass(this.editorContainer, 'synthetic-focus'); } + hasFocus(): boolean { + return this.inputEditor.hasWidgetFocus(); + } + private renderValidation(): void { this.validationDisposable.dispose(); @@ -1532,7 +1595,12 @@ class SCMInputWidget extends Disposable { return this.defaultInputFontFamily; } + clearValidation(): void { + this.validationDisposable.dispose(); + } + dispose(): void { + this.input = undefined; this.repositoryDisposables.dispose(); this.validationDisposable.dispose(); super.dispose(); @@ -1582,6 +1650,8 @@ export class SCMViewPane extends ViewPane { private listLabels!: ResourceLabels; private menus!: SCMMenus; private inputRenderer!: InputRenderer; + private genericTitleMenu: SCMTitleMenu; + private toggleViewModelModeAction: ToggleViewModeAction | undefined; constructor( options: IViewPaneOptions, @@ -1591,7 +1661,6 @@ export class SCMViewPane extends ViewPane { @IContextMenuService protected contextMenuService: IContextMenuService, @IContextViewService protected contextViewService: IContextViewService, @ICommandService protected commandService: ICommandService, - @INotificationService private readonly notificationService: INotificationService, @IEditorService protected editorService: IEditorService, @IInstantiationService protected instantiationService: IInstantiationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @@ -1604,6 +1673,9 @@ 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)); } protected renderBody(container: HTMLElement): void { @@ -1613,7 +1685,7 @@ export class SCMViewPane extends ViewPane { this.listContainer = append(container, $('.scm-view.show-file-icons')); const updateActionsVisibility = () => toggleClass(this.listContainer, 'show-actions', this.configurationService.getValue('scm.alwaysShowActions')); - Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowActions'))(updateActionsVisibility); + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowActions'))(updateActionsVisibility)); updateActionsVisibility(); const updateProviderCountVisibility = () => { @@ -1621,7 +1693,7 @@ export class SCMViewPane extends ViewPane { toggleClass(this.listContainer, 'hide-provider-counts', value === 'hidden'); toggleClass(this.listContainer, 'auto-provider-counts', value === 'auto'); }; - Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.providerCountBadge'))(updateProviderCountVisibility); + 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); @@ -1629,10 +1701,11 @@ export class SCMViewPane extends ViewPane { 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.inputRenderer = this.instantiationService.createInstance(InputRenderer, this.layoutCache, (input, height) => this.tree.updateElementHeight(input, height), () => this.tree.domFocus()); + this.inputRenderer = this.instantiationService.createInstance(InputRenderer, this.layoutCache, (input, height) => this.tree.updateElementHeight(input, height)); const delegate = new ProviderListDelegate(this.inputRenderer); const actionViewItemProvider = (action: IAction) => this.getActionViewItem(action); @@ -1679,6 +1752,7 @@ export class SCMViewPane extends ViewPane { this._register(this.tree.onDidOpen(this.open, this)); this._register(this.tree.onContextMenu(this.onListContextMenu, this)); + this._register(this.tree.onDidScroll(this.inputRenderer.clearValidation, this.inputRenderer)); this._register(this.tree); let viewMode = this.configurationService.getValue<'tree' | 'list'>('scm.defaultViewMode') === 'list' ? ViewModelMode.List : ViewModelMode.Tree; @@ -1698,8 +1772,12 @@ export class SCMViewPane extends ViewPane { this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); this._register(this.viewModel.onDidChangeMode(this.onDidChangeMode, this)); + this.toggleViewModelModeAction = new ToggleViewModeAction(ToggleViewModeAction.ID, ToggleViewModeAction.LABEL, this.viewModel); + this._register(this.toggleViewModelModeAction); + this._register(this.onDidChangeBodyVisibility(this.viewModel.setVisible, this.viewModel)); + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowRepositories'))(this.updateActions, this)); this.updateActions(); } @@ -1741,15 +1819,22 @@ export class SCMViewPane extends ViewPane { } getActions(): IAction[] { + const result = []; + + if (this.toggleViewModelModeAction) { + result.push(this.toggleViewModelModeAction); + } + if (!this.viewModel) { - return []; + return result; } if (this.viewModel.repositories.elements.length < 2) { - return this.viewModel.getViewActions(); + return [...result, ...this.viewModel.getViewActions()]; } return [ + ...result, new SCMCollapseAction(this.viewModel), ...this.viewModel.getViewActions() ]; @@ -1768,11 +1853,7 @@ export class SCMViewPane extends ViewPane { return new StatusBarActionViewItem(action); } - if (!(action instanceof MenuItemAction)) { - return undefined; - } - - return new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + return super.getActionViewItem(action); } getActionsContext(): any { @@ -1842,27 +1923,33 @@ export class SCMViewPane extends ViewPane { const element = e.element; let context: any = element; let actions: IAction[] = []; + let disposable: IDisposable = Disposable.None; if (isSCMRepository(element)) { const menus = this.menus.getRepositoryMenus(element.provider); + const menu = menus.getRepositoryMenu(); context = element.provider; - actions = menus.getRepositoryContextActions(); + [actions, disposable] = collectContextMenuActions(menu, this.contextMenuService); } else if (isSCMInput(element)) { // noop } else if (isSCMResourceGroup(element)) { const menus = this.menus.getRepositoryMenus(element.provider); - actions = menus.getResourceGroupContextActions(element); + 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); - actions = menus.getResourceContextActions(element.element); + const menu = menus.getResourceMenu(element.element); + [actions, disposable] = collectContextMenuActions(menu, this.contextMenuService); } else { const menus = this.menus.getRepositoryMenus(element.context.provider); - actions = menus.getResourceFolderContextActions(element.context); + const menu = menus.getResourceFolderMenu(element.context); + [actions, disposable] = collectContextMenuActions(menu, this.contextMenuService); } } else { const menus = this.menus.getRepositoryMenus(element.resourceGroup.provider); - actions = menus.getResourceContextActions(element); + const menu = menus.getResourceMenu(element); + [actions, disposable] = collectContextMenuActions(menu, this.contextMenuService); } const actionRunner = new RepositoryPaneActionRunner(() => this.getSelectedResources()); @@ -1872,7 +1959,10 @@ export class SCMViewPane extends ViewPane { getAnchor: () => e.anchor, getActions: () => actions, getActionsContext: () => context, - actionRunner + actionRunner, + onHide() { + disposable.dispose(); + } }); } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPaneContainer.ts b/src/vs/workbench/contrib/scm/browser/scmViewPaneContainer.ts index b1efece39ac..d95d08c4bc7 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPaneContainer.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPaneContainer.ts @@ -6,47 +6,35 @@ import 'vs/css!./media/scm'; import { localize } from 'vs/nls'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { VIEWLET_ID, ISCMService } from 'vs/workbench/contrib/scm/common/scm'; +import { VIEWLET_ID } from 'vs/workbench/contrib/scm/common/scm'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { SCMRepositoryMenus } from './menus'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { INotificationService } from 'vs/platform/notification/common/notification'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { addClass } from 'vs/base/browser/dom'; +import { SCMViewPane } from 'vs/workbench/contrib/scm/browser/scmViewPane'; export class SCMViewPaneContainer extends ViewPaneContainer { constructor( @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @ITelemetryService telemetryService: ITelemetryService, - @ISCMService protected scmService: ISCMService, - @IInstantiationService protected instantiationService: IInstantiationService, - @IContextViewService protected contextViewService: IContextViewService, - @IKeybindingService protected keybindingService: IKeybindingService, - @INotificationService protected notificationService: INotificationService, - @IContextMenuService protected contextMenuService: IContextMenuService, - @IThemeService protected themeService: IThemeService, - @ICommandService protected commandService: ICommandService, + @IInstantiationService instantiationService: IInstantiationService, + @IContextMenuService contextMenuService: IContextMenuService, + @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @IConfigurationService configurationService: IConfigurationService, @IExtensionService extensionService: IExtensionService, - @IWorkspaceContextService protected contextService: IWorkspaceContextService, + @IWorkspaceContextService contextService: IWorkspaceContextService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService ) { super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); - - const menus = instantiationService.createInstance(SCMRepositoryMenus, undefined); - this._register(menus); - this._register(menus.onDidChangeTitle(this.updateTitleArea, this)); } create(parent: HTMLElement): void { @@ -54,6 +42,18 @@ export class SCMViewPaneContainer extends ViewPaneContainer { addClass(parent, 'scm-viewlet'); } + getActionsContext(): unknown { + if (this.views.length === 1) { + const view = this.views[0]; + + if (view instanceof SCMViewPane) { + return view.getActionsContext(); + } + } + + return undefined; + } + getOptimalWidth(): number { return 400; } diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index 08a7e69ce3a..1b6dcfda1f7 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -8,8 +8,9 @@ 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 { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { equals } from 'vs/base/common/arrays'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; export function isSCMRepository(element: any): element is ISCMRepository { return !!(element as ISCMRepository).provider && typeof (element as ISCMRepository).setSelected === 'function'; @@ -66,3 +67,10 @@ export function connectPrimaryMenuToInlineActionBar(menu: IMenu, actionBar: Acti actionBar.push(primary, { icon: true, label: false }); }, g => /^inline/.test(g)); } + +export function collectContextMenuActions(menu: IMenu, contextMenuService: IContextMenuService): [IAction[], IDisposable] { + const primary: IAction[] = []; + const actions: IAction[] = []; + const disposable = createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, { primary, secondary: actions }, contextMenuService, g => /^inline/.test(g)); + return [actions, disposable]; +} diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index cd96a61d4de..7ee09f52d8a 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -31,6 +31,7 @@ export interface ISCMResource { readonly resourceGroup: ISCMResourceGroup; readonly sourceUri: URI; readonly decorations: ISCMResourceDecorations; + readonly contextValue?: string; open(preserveFocus: boolean): Promise; } diff --git a/src/vs/workbench/contrib/search/browser/media/searchview.css b/src/vs/workbench/contrib/search/browser/media/searchview.css index 95326cf3528..8406dffe6fb 100644 --- a/src/vs/workbench/contrib/search/browser/media/searchview.css +++ b/src/vs/workbench/contrib/search/browser/media/searchview.css @@ -39,16 +39,16 @@ padding-left: 4px; } -.search-view .search-widget .monaco-inputbox > .wrapper > .mirror { - max-height: 134px; -} - /* NOTE: height is also used in searchWidget.ts as a constant*/ .search-view .search-widget .monaco-inputbox > .wrapper > textarea.input { overflow: initial; height: 24px; /* set initial height before measure */ } +.search-view .search-widget .monaco-findInput .monaco-scrollable-element .scrollbar { + opacity: 0; +} + .search-view .monaco-inputbox > .wrapper > textarea.input { scrollbar-width: none; /* Firefox: hide scrollbar */ } 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/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 17f12834563..ad1ba249fe8 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -798,7 +798,7 @@ export class SearchView extends ViewPane { } } - let navigator = this.tree.navigate(selected); + const navigator = this.tree.navigate(selected); let next = navigator.next(); if (!next) { @@ -1722,7 +1722,7 @@ export class SearchView extends ViewPane { const selections = fileMatch.matches().map(m => new Selection(m.range().startLineNumber, m.range().startColumn, m.range().endLineNumber, m.range().endColumn)); const codeEditor = getCodeEditor(editor.getControl()); if (codeEditor) { - let multiCursorController = MultiCursorSelectionController.get(codeEditor); + const multiCursorController = MultiCursorSelectionController.get(codeEditor); multiCursorController.selectAllUsingSelections(selections); } } diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index 61b1666716c..d900ecb01d7 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -308,7 +308,8 @@ export class SearchWidget extends Widget { appendWholeWordsLabel: appendKeyBindingLabel('', this.keyBindingService.lookupKeybinding(Constants.ToggleWholeWordCommandId), this.keyBindingService), appendRegexLabel: appendKeyBindingLabel('', this.keyBindingService.lookupKeybinding(Constants.ToggleRegexCommandId), this.keyBindingService), history: options.searchHistory, - flexibleHeight: true + flexibleHeight: true, + flexibleMaxHeight: 134 }; const searchInputContainer = dom.append(parent, dom.$('.search-container.input-box')); 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 d6aed7fb772..e5bbd712833 100644 --- a/src/vs/workbench/contrib/search/common/searchModel.ts +++ b/src/vs/workbench/contrib/search/common/searchModel.ts @@ -605,7 +605,7 @@ export class FolderMatch extends Disposable { fileMatches = [fileMatches]; } - for (let match of fileMatches as FileMatch[]) { + for (const match of fileMatches as FileMatch[]) { this._fileMatches.delete(match.resource); if (dispose) { match.dispose(); diff --git a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts index 39499ce4639..363eab383c1 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts @@ -28,7 +28,7 @@ suite('Search - Viewlet', () => { }); test('Data Source', function () { - let result: SearchResult = instantiation.createInstance(SearchResult, null); + const result: SearchResult = instantiation.createInstance(SearchResult, null); result.query = { type: QueryType.Text, contentPattern: { pattern: 'foo' }, @@ -58,20 +58,20 @@ suite('Search - Viewlet', () => { }] }]); - let fileMatch = result.matches()[0]; - let lineMatch = fileMatch.matches()[0]; + const fileMatch = result.matches()[0]; + const lineMatch = fileMatch.matches()[0]; assert.equal(fileMatch.id(), 'file:///c%3A/foo'); assert.equal(lineMatch.id(), 'file:///c%3A/foo>[2,1 -> 2,2]b'); }); test('Comparer', () => { - let fileMatch1 = aFileMatch(isWindows ? 'C:\\foo' : '/c/foo'); - let fileMatch2 = aFileMatch(isWindows ? 'C:\\with\\path' : '/c/with/path'); - let fileMatch3 = aFileMatch(isWindows ? 'C:\\with\\path\\foo' : '/c/with/path/foo'); - let lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); - let lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); - let lineMatch3 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const fileMatch1 = aFileMatch(isWindows ? 'C:\\foo' : '/c/foo'); + const fileMatch2 = aFileMatch(isWindows ? 'C:\\with\\path' : '/c/with/path'); + const fileMatch3 = aFileMatch(isWindows ? 'C:\\with\\path\\foo' : '/c/with/path/foo'); + const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); + const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch3 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); assert(searchMatchComparer(fileMatch1, fileMatch2) < 0); assert(searchMatchComparer(fileMatch2, fileMatch1) > 0); @@ -84,10 +84,10 @@ suite('Search - Viewlet', () => { }); test('Advanced Comparer', () => { - let fileMatch1 = aFileMatch(isWindows ? 'C:\\with\\path\\foo10' : '/c/with/path/foo10'); - let fileMatch2 = aFileMatch(isWindows ? 'C:\\with\\path2\\foo1' : '/c/with/path2/foo1'); - let fileMatch3 = aFileMatch(isWindows ? 'C:\\with\\path2\\bar.a' : '/c/with/path2/bar.a'); - let fileMatch4 = aFileMatch(isWindows ? 'C:\\with\\path2\\bar.b' : '/c/with/path2/bar.b'); + const fileMatch1 = aFileMatch(isWindows ? 'C:\\with\\path\\foo10' : '/c/with/path/foo10'); + const fileMatch2 = aFileMatch(isWindows ? 'C:\\with\\path2\\foo1' : '/c/with/path2/foo1'); + const fileMatch3 = aFileMatch(isWindows ? 'C:\\with\\path2\\bar.a' : '/c/with/path2/bar.a'); + const fileMatch4 = aFileMatch(isWindows ? 'C:\\with\\path2\\bar.b' : '/c/with/path2/bar.b'); // By default, path < path2 assert(searchMatchComparer(fileMatch1, fileMatch2) < 0); @@ -98,7 +98,7 @@ suite('Search - Viewlet', () => { }); function aFileMatch(path: string, searchResult?: SearchResult, ...lineMatches: ITextSearchMatch[]): FileMatch { - let rawMatch: IFileMatch = { + const rawMatch: IFileMatch = { resource: uri.file(path), results: lineMatches }; diff --git a/src/vs/workbench/contrib/search/test/common/extractRange.test.ts b/src/vs/workbench/contrib/search/test/common/extractRange.test.ts index bd96b29baa4..a60f8eea965 100644 --- a/src/vs/workbench/contrib/search/test/common/extractRange.test.ts +++ b/src/vs/workbench/contrib/search/test/common/extractRange.test.ts @@ -36,7 +36,7 @@ suite('extractRangeFromFilter', () => { }); test('allow space after path', async function () { - let res = extractRangeFromFilter('/some/path/file.txt (19,20)'); + const res = extractRangeFromFilter('/some/path/file.txt (19,20)'); assert.equal(res?.filter, '/some/path/file.txt'); assert.equal(res?.range.startLineNumber, 19); @@ -44,7 +44,7 @@ suite('extractRangeFromFilter', () => { }); test('unless', async function () { - let res = extractRangeFromFilter('/some/path/file.txt@ (19,20)', ['@']); + const res = extractRangeFromFilter('/some/path/file.txt@ (19,20)', ['@']); assert.ok(!res); }); diff --git a/src/vs/workbench/contrib/searchEditor/browser/constants.ts b/src/vs/workbench/contrib/searchEditor/browser/constants.ts index cc24ea515f6..b37c41a7a2d 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/constants.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/constants.ts @@ -14,3 +14,4 @@ export const SearchEditorFindMatchClass = 'seaarchEditorFindMatch'; export const SearchEditorID = 'workbench.editor.searchEditor'; export const OpenNewEditorCommandId = 'search.action.openNewEditor'; +export const OpenEditorCommandId = 'search.action.openEditor'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index e18d33ceb73..711a8b6a5d9 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -213,7 +213,7 @@ CommandsRegistry.registerCommand( //#region Actions const category = { value: localize('search', "Search Editor"), original: 'Search Editor' }; -export type OpenSearchEditorArgs = Partial; +export type OpenSearchEditorArgs = Partial; const openArgDescription = { description: 'Open a new search editor. Arguments passed can include variables like ${relativeFileDirname}.', args: [{ @@ -264,14 +264,29 @@ registerAction2(class extends Action2 { constructor() { super({ id: SearchEditorConstants.OpenNewEditorCommandId, - title: { value: localize('search.openNewSearchEditor', "Open new Search Editor"), original: 'Open new Search Editor' }, + title: { value: localize('search.openNewSearchEditor', "New Search Editor"), original: 'New Search Editor' }, category, f1: true, description: openArgDescription }); } async run(accessor: ServicesAccessor, args: OpenSearchEditorArgs) { - await accessor.get(IInstantiationService).invokeFunction(openNewSearchEditor, args); + await accessor.get(IInstantiationService).invokeFunction(openNewSearchEditor, { ...args, location: 'new' }); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: SearchEditorConstants.OpenEditorCommandId, + title: { value: localize('search.openSearchEditor', "Open Search Editor"), original: 'Open Search Editor' }, + category, + f1: true, + description: openArgDescription + }); + } + async run(accessor: ServicesAccessor, args: OpenSearchEditorArgs) { + await accessor.get(IInstantiationService).invokeFunction(openNewSearchEditor, { ...args, location: 'reuse' }); } }); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 22d3cf6d0a5..ae1538b3ec8 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -202,10 +202,6 @@ export class SearchEditor extends BaseTextEditor { } } - protected getConfigurationOverrides() { - return { ...super.getConfigurationOverrides(), links: false }; - } - private createResultsEditor(parent: HTMLElement) { const searchResultContainer = DOM.append(parent, DOM.$('.search-results')); super.createEditor(searchResultContainer); @@ -393,7 +389,7 @@ export class SearchEditor extends BaseTextEditor { const matchText = model.getValueInRange(matchRange); let file = ''; for (let line = matchRange.startLineNumber; line >= 1; line--) { - let lineText = model.getValueInRange(new Range(line, 1, line, 2)); + const lineText = model.getValueInRange(new Range(line, 1, line, 2)); if (lineText !== ' ') { file = model.getLineContent(line); break; } } alert(localize('searchResultItem', "Matched {0} at {1} in file {2}", matchText, matchLineText, file.slice(0, file.length - 1))); @@ -505,10 +501,11 @@ export class SearchEditor extends BaseTextEditor { return; } + const sortOrder = this.configurationService.getValue('search').sortOrder; 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); + const results = serializeSearchResultForEditor(this.searchModel.searchResult, config.includes, config.excludes, config.contextLines, labelFormatter, sortOrder); const { body } = await input.getModels(); this.modelService.updateModel(body, results.text); input.config = config; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts index 0b202a5b062..0863b0b1c71 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -23,9 +23,11 @@ import { IConfigurationResolverService } from 'vs/workbench/services/configurati import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { Schemas } from 'vs/base/common/network'; -import { withNullAsUndefined } from 'vs/base/common/types'; +import { withNullAsUndefined, assertIsDefined } from 'vs/base/common/types'; import { OpenNewEditorCommandId } from 'vs/workbench/contrib/searchEditor/browser/constants'; import { OpenSearchEditorArgs } from 'vs/workbench/contrib/searchEditor/browser/searchEditor.contribution'; +import { EditorsOrder } from 'vs/workbench/common/editor'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; export const toggleSearchEditorCaseSensitiveCommand = (accessor: ServicesAccessor) => { const editorService = accessor.get(IEditorService); @@ -102,6 +104,7 @@ export class OpenSearchEditorAction extends Action { export const openNewSearchEditor = async (accessor: ServicesAccessor, _args: OpenSearchEditorArgs = {}, toSide = false) => { const editorService = accessor.get(IEditorService); + const editorGroupsService = accessor.get(IEditorGroupsService); const telemetryService = accessor.get(ITelemetryService); const instantiationService = accessor.get(IInstantiationService); const configurationService = accessor.get(IConfigurationService); @@ -136,13 +139,20 @@ export const openNewSearchEditor = telemetryService.publicLog2('searchEditor/openNewSearchEditor'); - const args: Record = { query: selected }; + const args: OpenSearchEditorArgs = { query: selected }; Object.entries(_args).forEach(([name, value]) => { - args[name as any] = (typeof value === 'string') ? configurationResolverService.resolve(lastActiveWorkspaceRoot, value) : value; + (args as any)[name as any] = (typeof value === 'string') ? configurationResolverService.resolve(lastActiveWorkspaceRoot, value) : value; }); - - const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config: args, text: '' }); - const editor = await editorService.openEditor(input, { pinned: true }, toSide ? SIDE_GROUP : ACTIVE_GROUP) as SearchEditor; + const existing = editorService.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).find(id => id.editor.getTypeId() === SearchEditorInput.ID); + let editor: SearchEditor; + 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(); + } else { + const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config: args, text: '' }); + editor = await editorService.openEditor(input, { pinned: true }, toSide ? SIDE_GROUP : ACTIVE_GROUP) as SearchEditor; + } const searchOnType = configurationService.getValue('search').searchOnType; if ( @@ -165,13 +175,14 @@ export const createEditorFromSearchResult = const instantiationService = accessor.get(IInstantiationService); const labelService = accessor.get(ILabelService); const configurationService = accessor.get(IConfigurationService); + const sortOrder = configurationService.getValue('search').sortOrder; telemetryService.publicLog2('searchEditor/createEditorFromSearchResult'); const labelFormatter = (uri: URI): string => labelService.getUriLabel(uri, { relative: true }); - const { text, matchRanges, config } = serializeSearchResultForEditor(searchResult, rawIncludePattern, rawExcludePattern, 0, labelFormatter); + const { text, matchRanges, config } = serializeSearchResultForEditor(searchResult, rawIncludePattern, rawExcludePattern, 0, labelFormatter, sortOrder); const contextLines = configurationService.getValue('search').searchEditor.defaultNumberOfContextLines; if (searchResult.isDirty || contextLines === 0 || contextLines === null) { diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index cb5f5c99735..c884a84be2b 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -305,7 +305,7 @@ export const getOrMakeSearchEditorInput = ( const priorConfig: SearchConfiguration = reuseOldSettings ? new Memento(SearchEditorInput.ID, storageService).getMemento(StorageScope.WORKSPACE).searchConfig : {}; const defaultConfig = defaultSearchConfig(); - let config = { ...defaultConfig, ...priorConfig, ...existingData.config }; + const config = { ...defaultConfig, ...priorConfig, ...existingData.config }; if (defaultNumberOfContextLines !== null && defaultNumberOfContextLines !== undefined) { config.contextLines = existingData.config.contextLines ?? defaultNumberOfContextLines; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts index eccc82f3e84..1bb2abc19ab 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts @@ -11,9 +11,9 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { Range } from 'vs/editor/common/core/range'; import type { ITextModel } from 'vs/editor/common/model'; import { localize } from 'vs/nls'; -import { FileMatch, Match, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; +import { FileMatch, Match, searchMatchComparer, SearchResult, FolderMatch } from 'vs/workbench/contrib/search/common/searchModel'; import type { SearchConfiguration } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; -import { ITextQuery } from 'vs/workbench/services/search/common/search'; +import { ITextQuery, SearchSortOrder } from 'vs/workbench/services/search/common/search'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; // Using \r\n on Windows inserts an extra newline between results. @@ -66,8 +66,8 @@ function fileMatchToSearchResultFormat(fileMatch: FileMatch, labelFormatter: (x: const serializedMatches = flatten(sortedMatches.map(match => matchToSearchResultFormat(match, longestLineNumber))); const uriString = labelFormatter(fileMatch.resource); - let text: string[] = [`${uriString}:`]; - let matchRanges: Range[] = []; + const text: string[] = [`${uriString}:`]; + const matchRanges: Range[] = []; const targetLineNumberToOffset: Record = {}; @@ -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): { matchRanges: Range[], text: string, config: Partial } => { + (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, contextLines: number, labelFormatter: (x: URI) => string, sortOrder: SearchSortOrder): { 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); @@ -221,11 +221,13 @@ export const serializeSearchResultForEditor = : localize('noResults', "No Results"), '']; + const matchComparer = (a: FileMatch | FolderMatch, b: FileMatch | FolderMatch) => searchMatchComparer(a, b, sortOrder); + const allResults = flattenSearchResultSerializations( flatten( - searchResult.folderMatches().sort(searchMatchComparer) - .map(folderMatch => folderMatch.matches().sort(searchMatchComparer) + searchResult.folderMatches().sort(matchComparer) + .map(folderMatch => folderMatch.matches().sort(matchComparer) .map(fileMatch => fileMatchToSearchResultFormat(fileMatch, labelFormatter))))); return { @@ -236,8 +238,8 @@ export const serializeSearchResultForEditor = }; const flattenSearchResultSerializations = (serializations: SearchResultSerialization[]): SearchResultSerialization => { - let text: string[] = []; - let matchRanges: Range[] = []; + const text: string[] = []; + const matchRanges: Range[] = []; serializations.forEach(serialized => { serialized.matchRanges.map(translateRangeLines(text.length)).forEach(range => matchRanges.push(range)); diff --git a/src/vs/workbench/contrib/tags/electron-browser/workspaceTagsService.ts b/src/vs/workbench/contrib/tags/electron-browser/workspaceTagsService.ts index 2bf416bf119..a196460434f 100644 --- a/src/vs/workbench/contrib/tags/electron-browser/workspaceTagsService.ts +++ b/src/vs/workbench/contrib/tags/electron-browser/workspaceTagsService.ts @@ -22,6 +22,19 @@ import { IWorkspaceTagsService, Tags } from 'vs/workbench/contrib/tags/common/wo import { getHashedRemotesFromConfig } from 'vs/workbench/contrib/tags/electron-browser/workspaceTags'; import { IProductService } from 'vs/platform/product/common/productService'; +const MetaModulesToLookFor = [ + // Azure packages + '@azure', + '@azure/ai', + '@azure/core', + '@azure/cosmos', + '@azure/event', + '@azure/identity', + '@azure/keyvault', + '@azure/search', + '@azure/storage' +]; + const ModulesToLookFor = [ // Packages that suggest a node server 'express', @@ -33,6 +46,8 @@ const ModulesToLookFor = [ // JS frameworks 'react', 'react-native', + 'react-native-macos', + 'react-native-windows', 'rnpm-plugin-windows', '@angular/core', '@ionic', @@ -48,6 +63,7 @@ const ModulesToLookFor = [ '@google-cloud/common', 'heroku-cli', //Office and Sharepoint packages + '@microsoft/teams-js', '@microsoft/office-js', '@microsoft/office-js-helpers', '@types/office-js', @@ -69,27 +85,35 @@ const ModulesToLookFor = [ 'playwright-firefox', 'playwright-webkit' ]; + +const PyMetaModulesToLookFor = [ + 'azure-ai', + 'azure-cognitiveservices', + 'azure-core', + 'azure-cosmos', + 'azure-event', + 'azure-identity', + 'azure-keyvault', + 'azure-mgmt', + 'azure-ml', + 'azure-search', + 'azure-storage' +]; + const PyModulesToLookFor = [ 'azure', - 'azure-storage-common', - 'azure-storage-blob', - 'azure-storage-file', - 'azure-storage-queue', - 'azure-shell', - 'azure-cosmos', 'azure-devtools', 'azure-elasticluster', 'azure-eventgrid', 'azure-functions', 'azure-graphrbac', - 'azure-keyvault', + 'azure-iothub-device-client', 'azure-loganalytics', 'azure-monitor', 'azure-servicebus', 'azure-servicefabric', - 'azure-storage', + 'azure-shell', 'azure-translator', - 'azure-iothub-device-client', 'adal', 'pydocumentdb', 'botbuilder-core', @@ -186,11 +210,21 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.npm.vue" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.aws-sdk" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.aws-amplify-sdk" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/ai" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/core" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/cosmos" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/event" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/identity" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/keyvault" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/search" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/storage" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.azure" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.azure-storage" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@google-cloud/common" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.firebase" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.heroku-cli" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@microsoft/teams-js" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@microsoft/office-js" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@microsoft/office-js-helpers" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@types/office-js" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -211,6 +245,8 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.npm.playwright-chromium" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.playwright-firefox" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.playwright-webkit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.react-native-macos" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.react-native-windows" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.bower" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.yeoman.code.ext" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.cordova.high" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -227,30 +263,31 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.py.Pipfile" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.conda" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.any-azure" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "workspace.py.azure" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "workspace.py.azure-storage-common" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "workspace.py.azure-storage-blob" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "workspace.py.azure-storage-file" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "workspace.py.azure-storage-queue" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "workspace.py.azure-mgmt" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "workspace.py.azure-shell" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.pulumi-azure" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-ai" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-cognitiveservices" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-core" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-cosmos" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-devtools" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-elasticluster" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-event" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-eventgrid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-functions" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-graphrbac" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-identity" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-iothub-device-client" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-keyvault" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-loganalytics" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-mgmt" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-ml" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-monitor" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-search" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-servicebus" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-servicefabric" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-shell" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-storage" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-translator" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "workspace.py.azure-iothub-device-client" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "workspace.py.azure-ml" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "workspace.py.azure-cognitiveservices" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.adal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.pydocumentdb" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.botbuilder-core" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -370,16 +407,13 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { if (PyModulesToLookFor.indexOf(packageName) > -1) { tags['workspace.py.' + packageName] = true; } - // cognitive services has a lot of tiny packages. e.g. 'azure-cognitiveservices-search-autosuggest' - if (packageName.indexOf('azure-cognitiveservices') > -1) { - tags['workspace.py.azure-cognitiveservices'] = true; - } - if (packageName.indexOf('azure-mgmt') > -1) { - tags['workspace.py.azure-mgmt'] = true; - } - if (packageName.indexOf('azure-ml') > -1) { - tags['workspace.py.azure-ml'] = true; + + for (const metaModule of PyMetaModulesToLookFor) { + if (packageName.startsWith(metaModule)) { + tags['workspace.py.' + metaModule] = true; + } } + if (!tags['workspace.py.any-azure']) { tags['workspace.py.any-azure'] = /azure/i.test(packageName); } @@ -419,24 +453,23 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { const packageJsonPromises = getFilePromises('package.json', this.fileService, this.textFileService, content => { try { const packageJsonContents = JSON.parse(content.value); - let dependencies = packageJsonContents['dependencies']; - let devDependencies = packageJsonContents['devDependencies']; - for (let module of ModulesToLookFor) { - if ('react-native' === module) { - if ((dependencies && dependencies[module]) || (devDependencies && devDependencies[module])) { - tags['workspace.reactNative'] = true; - } - } else if ('tns-core-modules' === module) { - if ((dependencies && dependencies[module]) || (devDependencies && devDependencies[module])) { - tags['workspace.nativescript'] = true; - } + let dependencies = Object.keys(packageJsonContents['dependencies'] || {}).concat(Object.keys(packageJsonContents['devDependencies'] || {})); + + for (let dependency of dependencies) { + if ('react-native' === dependency) { + tags['workspace.reactNative'] = true; + } else if ('tns-core-modules' === dependency) { + tags['workspace.nativescript'] = true; + } else if (ModulesToLookFor.indexOf(dependency) > -1) { + tags['workspace.npm.' + dependency] = true; } else { - if ((dependencies && dependencies[module]) || (devDependencies && devDependencies[module])) { - tags['workspace.npm.' + module] = true; + for (const metaModule of MetaModulesToLookFor) { + if (dependency.startsWith(metaModule)) { + tags['workspace.npm.' + metaModule] = true; + } } } } - } catch (e) { // Ignore errors when resolving file or parsing file contents diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 1f84fabf713..e757824eb43 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -80,6 +80,7 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance import { IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; import { isWorkspaceFolder, TaskQuickPickEntry, QUICKOPEN_DETAIL_CONFIG, TaskQuickPick, QUICKOPEN_SKIP_CONFIG } from 'vs/workbench/contrib/tasks/browser/taskQuickPick'; import { ILogService } from 'vs/platform/log/common/log'; +import { once } from 'vs/base/common/functional'; const QUICKOPEN_HISTORY_LIMIT_CONFIG = 'task.quickOpen.history'; const PROBLEM_MATCHER_NEVER_CONFIG = 'task.problemMatchers.neverPrompt'; @@ -223,6 +224,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer protected _outputChannel: IOutputChannel; protected readonly _onDidStateChange: Emitter; + private _waitForSupportedExecutions: Promise; + private _onDidRegisterSupportedExecutions: Emitter = new Emitter(); constructor( @IConfigurationService private readonly configurationService: IConfigurationService, @@ -331,16 +334,26 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } return task._label; }); - this.setExecutionContexts(); + + this._waitForSupportedExecutions = new Promise(resolve => { + once(this._onDidRegisterSupportedExecutions.event)(() => resolve()); + }); } - protected setExecutionContexts(custom: boolean = true, shell: boolean = false, process: boolean = false): void { - const customContext = CustomExecutionSupportedContext.bindTo(this.contextKeyService); - customContext.set(custom); - const shellContext = ShellExecutionSupportedContext.bindTo(this.contextKeyService); - shellContext.set(shell); - const processContext = ProcessExecutionSupportedContext.bindTo(this.contextKeyService); - processContext.set(process); + public registerSupportedExecutions(custom?: boolean, shell?: boolean, process?: boolean) { + if (custom !== undefined) { + const customContext = CustomExecutionSupportedContext.bindTo(this.contextKeyService); + customContext.set(custom); + } + if (shell !== undefined) { + const shellContext = ShellExecutionSupportedContext.bindTo(this.contextKeyService); + shellContext.set(shell); + } + if (process !== undefined) { + const processContext = ProcessExecutionSupportedContext.bindTo(this.contextKeyService); + processContext.set(process); + } + this._onDidRegisterSupportedExecutions.fire(); } public get onDidStateChange(): Event { @@ -831,7 +844,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?LinkId=733558')); } - public build(): Promise { + public async build(): Promise { return this.getGroupedTasks().then((tasks) => { let runnable = this.createRunnableTask(tasks, TaskGroup.Build); if (!runnable || !runnable.task) { @@ -841,7 +854,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer throw new TaskError(Severity.Info, nls.localize('TaskService.noBuildTask2', 'No build task defined. Mark a task with as a \'build\' group in the tasks.json file.'), TaskErrors.NoBuildTask); } } - return this.executeTask(runnable.task, runnable.resolver); + return this.executeTask(runnable.task, runnable.resolver, TaskRunSource.User); }).then(value => value, (error) => { this.handleError(error); return Promise.reject(error); @@ -858,7 +871,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer throw new TaskError(Severity.Info, nls.localize('TaskService.noTestTask2', 'No test task defined. Mark a task with as a \'test\' group in the tasks.json file.'), TaskErrors.NoTestTask); } } - return this.executeTask(runnable.task, runnable.resolver); + return this.executeTask(runnable.task, runnable.resolver, TaskRunSource.User); }).then(value => value, (error) => { this.handleError(error); return Promise.reject(error); @@ -875,12 +888,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (options && options.attachProblemMatcher && this.shouldAttachProblemMatcher(task) && !InMemoryTask.is(task)) { const toExecute = await this.attachProblemMatcher(task); if (toExecute) { - resolve(this.executeTask(toExecute, resolver)); + resolve(this.executeTask(toExecute, resolver, runSource)); } else { resolve(undefined); } } else { - resolve(this.executeTask(task, resolver)); + resolve(this.executeTask(task, resolver, runSource)); } }).then((value) => { if (runSource === TaskRunSource.User) { @@ -1449,7 +1462,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }; } - private executeTask(task: Task, resolver: ITaskResolver): Promise { + private executeTask(task: Task, resolver: ITaskResolver, runSource: TaskRunSource): Promise { enum SaveBeforeRunConfigOptions { Always = 'always', Never = 'never', @@ -1461,7 +1474,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer const execTask = async (task: Task, resolver: ITaskResolver): Promise => { return ProblemMatcherRegistry.onReady().then(() => { let executeResult = this.getTaskSystem().run(task, resolver); - return this.handleExecuteResult(executeResult); + return this.handleExecuteResult(executeResult, runSource); }); }; @@ -1498,7 +1511,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } } - private async handleExecuteResult(executeResult: ITaskExecuteResult): Promise { + private async handleExecuteResult(executeResult: ITaskExecuteResult, runSource?: TaskRunSource): Promise { if (executeResult.task.taskLoadMessages && executeResult.task.taskLoadMessages.length > 0) { executeResult.task.taskLoadMessages.forEach(loadMessage => { this._outputChannel.append(loadMessage + '\n'); @@ -1506,7 +1519,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this.showOutput(); } - await this.setRecentlyUsedTask(executeResult.task); + if (runSource === TaskRunSource.User) { + await this.setRecentlyUsedTask(executeResult.task); + } if (executeResult.kind === TaskExecuteKind.Active) { let active = executeResult.active; if (active && active.same) { @@ -1826,7 +1841,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return result; } - public getWorkspaceTasks(runSource: TaskRunSource = TaskRunSource.User): Promise> { + public async getWorkspaceTasks(runSource: TaskRunSource = TaskRunSource.User): Promise> { + await this._waitForSupportedExecutions; if (this._workspaceTasksPromise) { return this._workspaceTasksPromise; } diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index c5b1ed19e29..f6993a12c3c 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -446,7 +446,7 @@ export class TerminalTaskSystem implements ITaskSystem { let promise = this.activeTasks[key] ? this.activeTasks[key].promise : undefined; if (!promise) { this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.DependsOnStarted, task)); - promise = this.executeTask(dependencyTask, resolver, trigger, alreadyResolved); + promise = this.executeDependencyTask(dependencyTask, resolver, trigger, alreadyResolved); } promises.push(promise); if (task.configurationProperties.dependsOrder === DependsOrder.sequence) { @@ -496,6 +496,24 @@ export class TerminalTaskSystem implements ITaskSystem { } } + private async executeDependencyTask(task: Task, resolver: ITaskResolver, trigger: string, alreadyResolved?: Map): Promise { + // If the task is a background task with a watching problem matcher, we don't wait for the whole task to finish, + // just for the problem matcher to go inactive. + if (!task.configurationProperties.isBackground) { + return this.executeTask(task, resolver, trigger, alreadyResolved); + } + + const inactivePromise = new Promise(resolve => { + const taskInactiveDisposable = this._onDidStateChange.event(taskEvent => { + if ((taskEvent.kind === TaskEventKind.Inactive) && (taskEvent.__task === task)) { + taskInactiveDisposable.dispose(); + resolve({ exitCode: 0 }); + } + }); + }); + return Promise.race([inactivePromise, this.executeTask(task, resolver, trigger, alreadyResolved)]); + } + private async resolveAndFindExecutable(systemInfo: TaskSystemInfo | undefined, workspaceFolder: IWorkspaceFolder | undefined, task: CustomTask | ContributedTask, cwd: string | undefined, envPath: string | undefined): Promise { const command = this.configurationResolverService.resolve(workspaceFolder, CommandString.value(task.command.name!)); cwd = cwd ? this.configurationResolverService.resolve(workspaceFolder, cwd) : undefined; @@ -528,7 +546,7 @@ export class TerminalTaskSystem implements ITaskSystem { } } - private resolveVariablesFromSet(taskSystemInfo: TaskSystemInfo | undefined, workspaceFolder: IWorkspaceFolder | undefined, task: CustomTask | ContributedTask, variables: Set, alreadyResolved: Map): Promise { + private resolveVariablesFromSet(taskSystemInfo: TaskSystemInfo | undefined, workspaceFolder: IWorkspaceFolder | undefined, task: CustomTask | ContributedTask, variables: Set, alreadyResolved: Map): Promise { let isProcess = task.command && task.command.runtime === RuntimeType.Process; let options = task.command && task.command.options ? task.command.options : undefined; let cwd = options ? options.cwd : undefined; @@ -544,7 +562,7 @@ export class TerminalTaskSystem implements ITaskSystem { } } const unresolved = this.findUnresolvedVariables(variables, alreadyResolved); - let resolvedVariables: Promise; + let resolvedVariables: Promise; if (taskSystemInfo && workspaceFolder) { let resolveSet: ResolveSet = { variables: unresolved @@ -560,6 +578,10 @@ export class TerminalTaskSystem implements ITaskSystem { } } resolvedVariables = taskSystemInfo.resolveVariables(workspaceFolder, resolveSet, TaskSourceKind.toConfigurationTarget(task._source.kind)).then(async (resolved) => { + if (!resolved) { + return undefined; + } + this.mergeMaps(alreadyResolved, resolved.variables); resolved.variables = new Map(alreadyResolved); if (isProcess) { @@ -569,14 +591,14 @@ export class TerminalTaskSystem implements ITaskSystem { } resolved.variables.set(TerminalTaskSystem.ProcessVarName, process); } - return Promise.resolve(resolved); + return resolved; }); return resolvedVariables; } else { let variablesArray = new Array(); unresolved.forEach(variable => variablesArray.push(variable)); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { this.configurationResolverService.resolveWithInteraction(workspaceFolder, variablesArray, 'tasks', undefined, TaskSourceKind.toConfigurationTarget(task._source.kind)).then(async (resolvedVariablesMap: Map | undefined) => { if (resolvedVariablesMap) { this.mergeMaps(alreadyResolved, resolvedVariablesMap); @@ -657,6 +679,9 @@ export class TerminalTaskSystem implements ITaskSystem { if (!hasAllVariables) { return this.resolveVariablesFromSet(lastTask.getVerifiedTask().systemInfo, lastTask.getVerifiedTask().workspaceFolder, task, variables, alreadyResolved).then((resolvedVariables) => { + if (!resolvedVariables) { + return { exitCode: 0 }; + } this.currentTask.resolvedVariables = resolvedVariables; return this.executeInTerminal(task, trigger, new VariableResolver(lastTask.getVerifiedTask().workspaceFolder, lastTask.getVerifiedTask().systemInfo, resolvedVariables.variables, this.configurationResolverService), workspaceFolder!); }, reason => { @@ -831,6 +856,7 @@ export class TerminalTaskSystem implements ITaskSystem { }); promise = new Promise((resolve, reject) => { const onExit = terminal!.onExit((exitCode) => { + onData.dispose(); onExit.dispose(); let key = task.getMapKey(); this.removeFromActiveTasks(task); @@ -861,12 +887,8 @@ export class TerminalTaskSystem implements ITaskSystem { // There is nothing else to do here. } } - // Hack to work around #92868 until terminal is fixed. - setTimeout(() => { - onData.dispose(); - startStopProblemMatcher.done(); - startStopProblemMatcher.dispose(); - }, 100); + startStopProblemMatcher.done(); + startStopProblemMatcher.dispose(); if (!processStartedSignaled && terminal) { this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal.processId!)); processStartedSignaled = true; @@ -1022,7 +1044,7 @@ export class TerminalTaskSystem implements ITaskSystem { } else { let commandExecutable = (task.command.runtime !== RuntimeType.CustomExecution) ? CommandString.value(command) : undefined; let executable = !isShellCommand - ? this.resolveVariable(variableResolver, '${' + TerminalTaskSystem.ProcessVarName + '}') + ? this.resolveVariable(variableResolver, this.resolveVariable(variableResolver, '${' + TerminalTaskSystem.ProcessVarName + '}')) : commandExecutable; // When we have a process task there is no need to quote arguments. So we go ahead and take the string value. diff --git a/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts b/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts index 4ec3b1db744..573a3c7a2d0 100644 --- a/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts +++ b/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts @@ -39,7 +39,8 @@ const taskDefinitionSchema: IJSONSchema = { }, when: { type: 'string', - markdownDescription: nls.localize('TaskDefinition.when', 'Condition when the task definition is valid. Consider using `shellExecutionSupported`, `processExecutionSupported`, and `customExecutionSupported` as appropriate for this task definition.') + markdownDescription: nls.localize('TaskDefinition.when', 'Condition which must be true to enable this type of task. Consider using `shellExecutionSupported`, `processExecutionSupported`, and `customExecutionSupported` as appropriate for this task definition.'), + default: '' } } }; diff --git a/src/vs/workbench/contrib/tasks/common/taskService.ts b/src/vs/workbench/contrib/tasks/common/taskService.ts index e456f8e7b53..7f395bdf770 100644 --- a/src/vs/workbench/contrib/tasks/common/taskService.ts +++ b/src/vs/workbench/contrib/tasks/common/taskService.ts @@ -95,6 +95,7 @@ export interface ITaskService { registerTaskProvider(taskProvider: ITaskProvider, type: string): IDisposable; registerTaskSystem(scheme: string, taskSystemInfo: TaskSystemInfo): void; + registerSupportedExecutions(custom?: boolean, shell?: boolean, process?: boolean): void; setJsonTasksSupported(areSuppored: Promise): void; extensionCallbackTaskComplete(task: Task, result: number | undefined): Promise; diff --git a/src/vs/workbench/contrib/tasks/common/taskSystem.ts b/src/vs/workbench/contrib/tasks/common/taskSystem.ts index 56bc04b325c..7f5b1758e18 100644 --- a/src/vs/workbench/contrib/tasks/common/taskSystem.ts +++ b/src/vs/workbench/contrib/tasks/common/taskSystem.ts @@ -119,7 +119,7 @@ export interface TaskSystemInfo { platform: Platform; context: any; uriProvider: (this: void, path: string) => URI; - resolveVariables(workspaceFolder: IWorkspaceFolder, toResolve: ResolveSet, target: ConfigurationTarget): Promise; + resolveVariables(workspaceFolder: IWorkspaceFolder, toResolve: ResolveSet, target: ConfigurationTarget): Promise; getDefaultShellAndArgs(): Promise<{ shell: string, args: string[] | string | undefined }>; findExecutable(command: string, cwd?: string, paths?: string[]): Promise; } diff --git a/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts b/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts index 16470daef7b..4dbac3fbf91 100644 --- a/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts +++ b/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts @@ -25,10 +25,6 @@ interface WorkspaceFolderConfigurationResult { export class TaskService extends AbstractTaskService { private _configHasErrors: boolean = false; - protected setExecutionContexts(): void { - super.setExecutionContexts(true, true, true); - } - protected getTaskSystem(): ITaskSystem { if (this._taskSystem) { return this._taskSystem; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index a6c7ebbcdd7..dd4b2216ff2 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -7,7 +7,6 @@ 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 { SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; 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'; @@ -39,6 +38,7 @@ import { localize } from 'vs/nls'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITerminalContributionService } from 'vs/workbench/contrib/terminal/common/terminalExtensionPoints'; +import { SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; async function getCwdForSplit(configHelper: ITerminalConfigHelper, instance: ITerminalInstance, folders?: IWorkspaceFolder[], commandService?: ICommandService): Promise { switch (configHelper.config.splitCwd) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 4b74eba639f..6ca70138147 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -72,6 +72,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _pressAnyKeyToCloseListener: IDisposable | undefined; private _id: number; + private _latestXtermWriteData: number = 0; + private _latestXtermParseData: number = 0; private _isExiting: boolean; private _hadFocusOnExit: boolean; private _isVisible: boolean; @@ -562,20 +564,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { setTimeout(() => this._refreshSelectionContextKey(), 0); })); - const xtermHelper: HTMLElement = xterm.element.querySelector('.xterm-helpers'); - const focusTrap: HTMLElement = document.createElement('div'); - focusTrap.setAttribute('tabindex', '0'); - dom.addClass(focusTrap, 'focus-trap'); - this._register(dom.addDisposableListener(focusTrap, 'focus', () => { - let currentElement = focusTrap; - while (!dom.hasClass(currentElement, 'part')) { - currentElement = currentElement.parentElement!; - } - const hidePanelElement = currentElement.querySelector('.hide-panel-action'); - hidePanelElement?.focus(); - })); - xtermHelper.insertBefore(focusTrap, xterm.textarea); - this._register(dom.addDisposableListener(xterm.textarea, 'focus', () => { this._terminalFocusContextKey.set(true); if (this.shellType) { @@ -840,6 +828,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { setTimeout(() => this.layout(this._timeoutDimension!), 0); } } + if (!visible) { + this._widgetManager.hideHovers(); + } } public scrollDownLine(): void { @@ -956,7 +947,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } private _onProcessData(data: string): void { - this._xterm?.write(data); + const messageId = ++this._latestXtermWriteData; + this._xterm?.write(data, () => this._latestXtermParseData = messageId); } /** @@ -965,15 +957,17 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { * @param exitCode The exit code of the process, this is undefined when the terminal was exited * through user action. */ - private _onProcessExit(exitCodeOrError?: number | ITerminalLaunchError): void { + private async _onProcessExit(exitCodeOrError?: number | ITerminalLaunchError): Promise { // Prevent dispose functions being triggered multiple times if (this._isExiting) { return; } + this._isExiting = true; + + await this._flushXtermData(); this._logService.debug(`Terminal process exit (id: ${this.id}) with code ${this._exitCode}`); - this._isExiting = true; let exitCodeMessage: string | undefined; // Create exit code message @@ -1058,6 +1052,24 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._onExit.fire(this._exitCode); } + /** + * Ensure write calls to xterm.js have finished before resolving. + */ + private _flushXtermData(): Promise { + if (this._latestXtermWriteData === this._latestXtermParseData) { + return Promise.resolve(); + } + let retries = 0; + return new Promise(r => { + const interval = setInterval(() => { + if (this._latestXtermWriteData === this._latestXtermParseData || ++retries === 5) { + clearInterval(interval); + r(); + } + }, 20); + }); + } + private _attachPressAnyKeyToCloseListener(xterm: XTermTerminal) { if (xterm.textarea && !this._pressAnyKeyToCloseListener) { this._pressAnyKeyToCloseListener = dom.addDisposableListener(xterm.textarea, 'keypress', (event: KeyboardEvent) => { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 2eeb515b6f0..55d0d5e1865 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -6,8 +6,7 @@ import * as dom from 'vs/base/browser/dom'; import * as nls from 'vs/nls'; import * as platform from 'vs/base/common/platform'; -import { Action, IAction } from 'vs/base/common/actions'; -import { IActionViewItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Action, IAction, Separator, IActionViewItem } from 'vs/base/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -110,6 +109,8 @@ export class TerminalViewPane extends ViewPane { } else { this.layoutBody(this._bodyDimensions.height, this._bodyDimensions.width); } + } else { + this._terminalService.getActiveTab()?.setVisible(false); } })); diff --git a/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts b/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts index 032610dbea7..b5bf843ffc7 100644 --- a/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts @@ -5,11 +5,15 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { ITerminalWidget } from 'vs/workbench/contrib/terminal/browser/widgets/widgets'; +import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; export class TerminalWidgetManager implements IDisposable { private _container: HTMLElement | undefined; private _attached: Map = new Map(); + constructor(@IHoverService private readonly _hoverService: IHoverService) { + } + attachToElement(terminalWrapper: HTMLElement) { if (!this._container) { this._container = document.createElement('div'); @@ -25,6 +29,10 @@ export class TerminalWidgetManager implements IDisposable { } } + hideHovers(): void { + this._hoverService.hideHover(); + } + attachWidget(widget: ITerminalWidget): IDisposable | undefined { if (!this._container) { return; diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 80c93d4f13b..f38353bcb8d 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -218,7 +218,7 @@ export const terminalConfiguration: IConfigurationNode = { default: false }, 'terminal.integrated.commandsToSkipShell': { - markdownDescription: localize('terminal.integrated.commandsToSkipShell', "A set of command IDs whose keybindings will not be sent to the shell but instead always be handled by VS Code. This allows keybindings that would normally be consumed by the shell to act instead the same as when the terminal is not focused, for example `Ctrl+P` to launch Quick Open.\n\n---\n\nMany commands are skipped by default. To override a default and pass that command's keybinding to the shell instead, add the command prefixed with the `-` character. For example add `-workbench.action.quickOpen` to allow `Ctrl+P` to reach the shell.\n\n*The following list of default skipped commands is truncated when viewed in Settings Editor. To see the full list, [open the default settings JSON](command:workbench.action.openRawDefaultSettings 'Open Default Settings (JSON)') and search for the first command from the list below.*\n\n---\n\nDefault Skipped Commands:\n\n{0}", DEFAULT_COMMANDS_TO_SKIP_SHELL.sort().map(command => `- ${command}`).join('\n')), + markdownDescription: localize('terminal.integrated.commandsToSkipShell', "A set of command IDs whose keybindings will not be sent to the shell but instead always be handled by VS Code. This allows keybindings that would normally be consumed by the shell to act instead the same as when the terminal is not focused, for example `Ctrl+P` to launch Quick Open.\n\n \n\nMany commands are skipped by default. To override a default and pass that command's keybinding to the shell instead, add the command prefixed with the `-` character. For example add `-workbench.action.quickOpen` to allow `Ctrl+P` to reach the shell.\n\n \n\nThe following list of default skipped commands is truncated when viewed in Settings Editor. To see the full list, [open the default settings JSON](command:workbench.action.openRawDefaultSettings 'Open Default Settings (JSON)') and search for the first command from the list below.\n\n \n\nDefault Skipped Commands:\n\n{0}", DEFAULT_COMMANDS_TO_SKIP_SHELL.sort().map(command => `- ${command}`).join('\n')), type: 'array', items: { type: 'string' diff --git a/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts b/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts index 1248e4b590b..b7ca9dbb216 100644 --- a/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts +++ b/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts @@ -10,7 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IViewsRegistry, IViewDescriptor, Extensions as ViewExtensions } from 'vs/workbench/common/views'; import { VIEW_CONTAINER } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { ITimelineService, TimelinePaneId } from 'vs/workbench/contrib/timeline/common/timeline'; -import { TimelineService } from 'vs/workbench/contrib/timeline/common/timelineService'; +import { TimelineHasProviderContext, TimelineService } from 'vs/workbench/contrib/timeline/common/timelineService'; import { TimelinePane } from './timelinePane'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -30,6 +30,7 @@ export class TimelinePaneDescriptor implements IViewDescriptor { readonly canToggleVisibility = true; readonly hideByDefault = false; readonly canMoveView = true; + readonly when = TimelineHasProviderContext; focusCommand = { id: 'timeline.focus' }; } diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index ada0c4da6bc..153a46de95e 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/timelinePane'; import { localize } from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; -import { IAction, ActionRunner } from 'vs/base/common/actions'; +import { IAction, ActionRunner, IActionViewItemProvider } from 'vs/base/common/actions'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { fromNow } from 'vs/base/common/date'; import { debounce } from 'vs/base/common/decorators'; @@ -36,10 +36,11 @@ import { IThemeService, LIGHT, ThemeIcon } from 'vs/platform/theme/common/themeS import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IActionViewItemProvider, ActionBar, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; -import { ContextAwareMenuEntryActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { MenuItemAction, IMenuService, MenuId, registerAction2, Action2, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { MenuEntryActionViewItem, createAndFillInContextMenuActions, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { MenuItemAction, IMenuService, MenuId, registerAction2, Action2, MenuRegistry, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; const ItemHeight = 22; @@ -1092,9 +1093,15 @@ class TimelineTreeRenderer implements ITreeRenderer action instanceof MenuItemAction - ? this.instantiationService.createInstance(ContextAwareMenuEntryActionViewItem, action) - : undefined; + this.actionViewItemProvider = (action: IAction) => { + if (action instanceof MenuItemAction) { + return this.instantiationService.createInstance(MenuEntryActionViewItem, action); + } else if (action instanceof SubmenuItemAction) { + return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); + } + + return undefined; + }; } private uri: URI | undefined; @@ -1239,7 +1246,6 @@ class TimelinePaneCommands extends Disposable { createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => /^inline/.test(g)); menu.dispose(); - scoped.dispose(); return result; } diff --git a/src/vs/workbench/contrib/timeline/common/timelineService.ts b/src/vs/workbench/contrib/timeline/common/timelineService.ts index 363d7f27f32..2d2d5e76d5a 100644 --- a/src/vs/workbench/contrib/timeline/common/timelineService.ts +++ b/src/vs/workbench/contrib/timeline/common/timelineService.ts @@ -11,6 +11,10 @@ import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { ITimelineService, TimelineChangeEvent, TimelineOptions, TimelineProvidersChangeEvent, TimelineProvider, InternalTimelineOptions, TimelinePaneId } from './timeline'; import { IViewsService } from 'vs/workbench/common/views'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +export const TimelineHasProviderContext = new RawContextKey('timelineHasProvider', false); export class TimelineService implements ITimelineService { declare readonly _serviceBrand: undefined; @@ -23,13 +27,30 @@ 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(); constructor( @ILogService private readonly logService: ILogService, @IViewsService protected viewsService: IViewsService, + @IConfigurationService protected configurationService: IConfigurationService, + @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'; // this.registerTimelineProvider({ // scheme: '*', @@ -213,6 +234,9 @@ export class TimelineService implements ITimelineService { } this.providers.set(id, provider); + + this.updateHasProviderContext(); + if (provider.onDidChange) { this.providerSubscriptions.set(id, provider.onDidChange(e => this._onDidChangeTimeline.fire(e))); } @@ -235,6 +259,9 @@ export class TimelineService implements ITimelineService { this.providers.delete(id); this.providerSubscriptions.delete(id); + + this.updateHasProviderContext(); + this._onDidChangeProviders.fire({ removed: [id] }); } @@ -242,4 +269,14 @@ export class TimelineService implements ITimelineService { this.viewsService.openView(TimelinePaneId, true); this._onDidChangeUri.fire(uri); } + + 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); + } } diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index 0a2a9f7bef9..5d590de2e93 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -55,8 +55,9 @@ export class OpenLatestReleaseNotesInBrowserAction extends Action { if (this.productService.releaseNotesUrl) { const uri = URI.parse(this.productService.releaseNotesUrl); await this.openerService.open(uri); + } else { + throw new Error(nls.localize('update.noReleaseNotesOnline', "This version of {0} does not have release notes online", this.productService.nameLong)); } - throw new Error('This version of Visual Studio Code does not have release notes online'); } } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts index b68f3a102a1..9b326f949d5 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +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'; @@ -18,6 +18,7 @@ import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/us export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { constructor( + @IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @IUserDataSyncService userDataSyncService: IUserDataSyncService, @@ -30,7 +31,7 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { @IStorageService storageService: IStorageService, @IEnvironmentService environmentService: IEnvironmentService, ) { - super(userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, userDataSyncMachinesService, storageService, environmentService); + super(userDataSyncStoreManagementService, userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, userDataSyncMachinesService, storageService, environmentService); this._register(Event.debounce(Event.any( Event.map(hostService.onDidChangeFocus, () => 'windowFocus'), diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts index edfd7b08df6..9a055fc2d7a 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts @@ -12,6 +12,7 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ 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'; class UserDataSyncReportIssueContribution extends Disposable implements IWorkbenchContribution { @@ -28,7 +29,7 @@ class UserDataSyncReportIssueContribution extends Disposable implements IWorkben case UserDataSyncErrorCode.LocalTooManyRequests: case UserDataSyncErrorCode.TooManyRequests: const operationId = error.operationId ? localize('operationId', "Operation Id: {0}", error.operationId) : undefined; - const message = localize('too many requests', "Turned off syncing preferences on this device because it is making too many requests."); + const message = localize('too many requests', "Turned off syncing settings on this device because it is making too many requests."); this.notificationService.notify({ severity: Severity.Error, message: operationId ? `${message} ${operationId}` : message, @@ -38,8 +39,34 @@ class UserDataSyncReportIssueContribution extends Disposable implements IWorkben } } +export class UserDataSyncSettingsMigrationContribution implements IWorkbenchContribution { + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + this.migrateSettings(); + } + + private async migrateSettings(): Promise { + await this.migrateSetting('sync.keybindingsPerPlatform', 'settingsSync.keybindingsPerPlatform'); + await this.migrateSetting('sync.ignoredExtensions', 'settingsSync.ignoredExtensions'); + await this.migrateSetting('sync.ignoredSettings', 'settingsSync.ignoredSettings'); + } + + private async migrateSetting(oldSetting: string, newSetting: string): Promise { + const userValue = this.configurationService.inspect(oldSetting).userValue; + if (userValue !== undefined) { + // remove the old setting + await this.configurationService.updateValue(oldSetting, undefined, ConfigurationTarget.USER); + // add the new setting + await this.configurationService.updateValue(newSetting, userValue, ConfigurationTarget.USER); + } + } +} + const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(UserDataSyncWorkbenchContribution, LifecyclePhase.Ready); +workbenchRegistry.registerWorkbenchContribution(UserDataSyncSettingsMigrationContribution, 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 35739d96ca5..049be0714d3 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -30,7 +30,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUserDataAutoSyncService, IUserDataSyncService, registerConfiguration, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncResourceEnablementService, - getSyncResourceFromLocalPreview, IResourcePreview + getSyncResourceFromLocalPreview, IResourcePreview, IUserDataSyncStoreManagementService, UserDataSyncStoreType } from 'vs/platform/userDataSync/common/userDataSync'; import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -53,7 +53,8 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Codicon } from 'vs/base/common/codicons'; import { ViewContainerLocation, IViewContainersRegistry, Extensions, ViewContainer } from 'vs/workbench/common/views'; import { UserDataSyncViewPaneContainer, UserDataSyncDataViews } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncViews'; -import { IUserDataSyncWorkbenchService, getSyncAreaLabel, AccountStatus, CONTEXT_SYNC_STATE, CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACCOUNT_STATE, CONFIGURE_SYNC_COMMAND_ID, SHOW_SYNC_LOG_COMMAND_ID, SYNC_VIEW_CONTAINER_ID } from 'vs/workbench/services/userDataSync/common/userDataSync'; +import { IUserDataSyncWorkbenchService, getSyncAreaLabel, AccountStatus, CONTEXT_SYNC_STATE, CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACCOUNT_STATE, CONFIGURE_SYNC_COMMAND_ID, SHOW_SYNC_LOG_COMMAND_ID, SYNC_VIEW_CONTAINER_ID, SYNC_TITLE } from 'vs/workbench/services/userDataSync/common/userDataSync'; +import { isNative } from 'vs/base/common/platform'; const CONTEXT_CONFLICTS_SOURCES = new RawContextKey('conflictsSources', ''); @@ -64,15 +65,15 @@ type SyncConflictsClassification = { action?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; }; -const turnOnSyncCommand = { id: 'workbench.userDataSync.actions.turnOn', title: localize('turn on sync with category', "Preferences Sync: Turn On...") }; -const turnOffSyncCommand = { id: 'workbench.userDataSync.actions.turnOff', title: localize('stop sync', "Preferences Sync: Turn Off") }; -const configureSyncCommand = { id: CONFIGURE_SYNC_COMMAND_ID, title: localize('configure sync', "Preferences Sync: Configure...") }; -const resolveSettingsConflictsCommand = { id: 'workbench.userDataSync.actions.resolveSettingsConflicts', title: localize('showConflicts', "Preferences Sync: Show Settings Conflicts") }; -const resolveKeybindingsConflictsCommand = { id: 'workbench.userDataSync.actions.resolveKeybindingsConflicts', title: localize('showKeybindingsConflicts', "Preferences Sync: Show Keybindings Conflicts") }; -const resolveSnippetsConflictsCommand = { id: 'workbench.userDataSync.actions.resolveSnippetsConflicts', title: localize('showSnippetsConflicts', "Preferences Sync: Show User Snippets Conflicts") }; +const turnOnSyncCommand = { id: 'workbench.userDataSync.actions.turnOn', title: localize('turn on sync with category', "{0}: Turn On...", SYNC_TITLE) }; +const turnOffSyncCommand = { id: 'workbench.userDataSync.actions.turnOff', title: localize('stop sync', "{0}: Turn Off", SYNC_TITLE) }; +const configureSyncCommand = { id: CONFIGURE_SYNC_COMMAND_ID, title: localize('configure sync', "{0}: Configure...", SYNC_TITLE) }; +const resolveSettingsConflictsCommand = { id: 'workbench.userDataSync.actions.resolveSettingsConflicts', title: localize('showConflicts', "{0}: Show Settings Conflicts", SYNC_TITLE) }; +const resolveKeybindingsConflictsCommand = { id: 'workbench.userDataSync.actions.resolveKeybindingsConflicts', title: localize('showKeybindingsConflicts', "{0}: Show Keybindings Conflicts", SYNC_TITLE) }; +const resolveSnippetsConflictsCommand = { id: 'workbench.userDataSync.actions.resolveSnippetsConflicts', title: localize('showSnippetsConflicts', "{0}: Show User Snippets Conflicts", SYNC_TITLE) }; const syncNowCommand = { id: 'workbench.userDataSync.actions.syncNow', - title: localize('sync now', "Preferences Sync: Sync Now"), + title: localize('sync now', "{0}: Sync Now", SYNC_TITLE), description(userDataSyncService: IUserDataSyncService): string | undefined { if (userDataSyncService.status === SyncStatus.Syncing) { return localize('syncing', "syncing"); @@ -83,8 +84,8 @@ const syncNowCommand = { return undefined; } }; -const showSyncSettingsCommand = { id: 'workbench.userDataSync.actions.settings', title: localize('sync settings', "Preferences Sync: Show Settings"), }; -const showSyncedDataCommand = { id: 'workbench.userDataSync.actions.showSyncedData', title: localize('show synced data', "Preferences Sync: Show Synced Data"), }; +const showSyncSettingsCommand = { id: 'workbench.userDataSync.actions.settings', title: localize('sync settings', "{0}: Show Settings", SYNC_TITLE), }; +const showSyncedDataCommand = { id: 'workbench.userDataSync.actions.showSyncedData', title: localize('show synced data', "{0}: Show Synced Data", SYNC_TITLE), }; const CONTEXT_TURNING_ON_STATE = new RawContextKey('userDataSyncTurningOn', false); @@ -111,20 +112,22 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo @IOutputService private readonly outputService: IOutputService, @IUserDataSyncAccountService readonly authTokenService: IUserDataSyncAccountService, @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, - @ITextModelService private readonly textModelResolverService: ITextModelService, + @ITextModelService textModelResolverService: ITextModelService, @IPreferencesService private readonly preferencesService: IPreferencesService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IProductService private readonly productService: IProductService, @IStorageService private readonly storageService: IStorageService, @IOpenerService private readonly openerService: IOpenerService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, + @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(); this.turningOnSyncContext = CONTEXT_TURNING_ON_STATE.bindTo(contextKeyService); this.conflictsSources = CONTEXT_CONFLICTS_SOURCES.bindTo(contextKeyService); - if (this.userDataSyncWorkbenchService.authenticationProviders.length) { + if (userDataSyncWorkbenchService.enabled) { registerConfiguration(); this.updateAccountBadge(); @@ -140,6 +143,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.updateGlobalActivityBadge(); })); this._register(userDataSyncService.onDidChangeConflicts(() => this.onDidChangeConflicts(this.userDataSyncService.conflicts))); + this._register(userDataAutoSyncService.onDidChangeEnablement(() => this.onDidChangeConflicts(this.userDataSyncService.conflicts))); this._register(userDataSyncService.onSyncErrors(errors => this.onSynchronizerErrors(errors))); this._register(userDataAutoSyncService.onError(error => this.onAutoSyncError(error))); @@ -148,9 +152,20 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo textModelResolverService.registerTextModelContentProvider(USER_DATA_SYNC_SCHEME, instantiationService.createInstance(UserDataRemoteContentProvider)); registerEditorContribution(AcceptChangesContribution.ID, AcceptChangesContribution); + + this._register(Event.any(userDataSyncService.onDidChangeStatus, userDataAutoSyncService.onDidChangeEnablement)(() => this.turningOnSync = !userDataAutoSyncService.isEnabled() && userDataSyncService.status !== SyncStatus.Idle)); } } + private get turningOnSync(): boolean { + return !!this.turningOnSyncContext.get(); + } + + private set turningOnSync(turningOn: boolean) { + this.turningOnSyncContext.set(turningOn); + this.updateGlobalActivityBadge(); + } + private readonly conflictsDisposables = new Map(); private onDidChangeConflicts(conflicts: [SyncResource, IResourcePreview[]][]) { if (!this.userDataAutoSyncService.isEnabled()) { @@ -241,12 +256,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private async acceptRemote(syncResource: SyncResource, conflicts: IResourcePreview[]) { try { for (const conflict of conflicts) { - const modelRef = await this.textModelResolverService.createModelReference(conflict.remoteResource); - try { - await this.userDataSyncService.accept(syncResource, conflict.remoteResource, modelRef.object.textEditorModel.getValue(), this.userDataAutoSyncService.isEnabled()); - } finally { - modelRef.dispose(); - } + await this.userDataSyncService.accept(syncResource, conflict.remoteResource, undefined, this.userDataAutoSyncService.isEnabled()); } } catch (e) { this.notificationService.error(e); @@ -256,12 +266,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private async acceptLocal(syncResource: SyncResource, conflicts: IResourcePreview[]): Promise { try { for (const conflict of conflicts) { - const modelRef = await this.textModelResolverService.createModelReference(conflict.previewResource); - try { - await this.userDataSyncService.accept(syncResource, conflict.previewResource, modelRef.object.textEditorModel.getValue(), this.userDataAutoSyncService.isEnabled()); - } finally { - modelRef.dispose(); - } + await this.userDataSyncService.accept(syncResource, conflict.localResource, undefined, this.userDataAutoSyncService.isEnabled()); } } catch (e) { this.notificationService.error(e); @@ -273,18 +278,18 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo case UserDataSyncErrorCode.SessionExpired: this.notificationService.notify({ severity: Severity.Info, - message: localize('session expired', "Preferences sync was turned off because current session is expired, please sign in again to turn on sync."), + message: localize('session expired', "Settings sync was turned off because current session is expired, please sign in again to turn on sync."), actions: { - primary: [new Action('turn on sync', localize('turn on sync', "Turn on Preferences Sync..."), undefined, true, () => this.turnOn())] + primary: [new Action('turn on sync', localize('turn on sync', "Turn on Settings Sync..."), undefined, true, () => this.turnOn())] } }); break; case UserDataSyncErrorCode.TurnedOff: this.notificationService.notify({ severity: Severity.Info, - message: localize('turned off', "Preferences sync was turned off from another device, please sign in again to turn on sync."), + message: localize('turned off', "Settings sync was turned off from another device, please sign in again to turn on sync."), actions: { - primary: [new Action('turn on sync', localize('turn on sync', "Turn on Preferences Sync..."), undefined, true, () => this.turnOn())] + primary: [new Action('turn on sync', localize('turn on sync', "Turn on Settings Sync..."), undefined, true, () => this.turnOn())] } }); break; @@ -298,7 +303,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo case UserDataSyncErrorCode.IncompatibleLocalContent: case UserDataSyncErrorCode.Gone: case UserDataSyncErrorCode.UpgradeRequired: - const message = localize('error upgrade required', "Preferences sync is disabled because the current version ({0}, {1}) is not compatible with the sync service. Please update before turning on sync.", this.productService.version, this.productService.commit); + const message = localize('error upgrade required', "Settings sync is disabled because the current version ({0}, {1}) is not compatible with the sync service. Please update before turning on sync.", this.productService.version, this.productService.commit); const operationId = error.operationId ? localize('operationId', "Operation Id: {0}", error.operationId) : undefined; this.notificationService.notify({ severity: Severity.Error, @@ -308,15 +313,23 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo case UserDataSyncErrorCode.IncompatibleRemoteContent: this.notificationService.notify({ severity: Severity.Error, - message: localize('error reset required', "Preferences sync is disabled because your data in the cloud is older than that of in the client. Please reset your data in the cloud before turning on sync."), + message: localize('error reset required', "Settings sync is disabled because your data in the cloud is older than that of the client. Please clear your data in the cloud before turning on sync."), actions: { primary: [ - new Action('reset', localize('reset', "Reset Synced Data"), undefined, true, () => this.userDataSyncWorkbenchService.resetSyncedData()), + new Action('reset', localize('reset', "Clear Data in Cloud..."), undefined, true, () => this.userDataSyncWorkbenchService.resetSyncedData()), new Action('show synced data', localize('show synced data action', "Show Synced Data"), undefined, true, () => this.userDataSyncWorkbenchService.showSyncActivity()) ] } }); return; + case UserDataSyncErrorCode.DefaultServiceChanged: + 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)."), + }); + } + return; } } @@ -389,10 +402,10 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo let clazz: string | undefined; let priority: number | undefined = undefined; - if (this.userDataSyncService.conflicts.length) { - badge = new NumberBadge(this.userDataSyncService.conflicts.reduce((result, [, conflicts]) => { return result + conflicts.length; }, 0), () => localize('has conflicts', "Preferences Sync: Conflicts Detected")); + if (this.userDataSyncService.conflicts.length && this.userDataAutoSyncService.isEnabled()) { + badge = new NumberBadge(this.userDataSyncService.conflicts.reduce((result, [, conflicts]) => { return result + conflicts.length; }, 0), () => localize('has conflicts', "{0}: Conflicts Detected", SYNC_TITLE)); } else if (this.turningOnSync) { - badge = new ProgressBadge(() => localize('turning on syncing', "Turning on Preferences Sync...")); + badge = new ProgressBadge(() => localize('turning on syncing', "Turning on Settings Sync...")); clazz = 'progress-badge'; priority = 1; } @@ -408,7 +421,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo let badge: IBadge | undefined = undefined; if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.userDataAutoSyncService.isEnabled() && this.userDataSyncWorkbenchService.accountStatus === AccountStatus.Unavailable) { - badge = new NumberBadge(1, () => localize('sign in to sync preferences', "Sign in to Sync Preferences")); + badge = new NumberBadge(1, () => localize('sign in to sync', "Sign in to Sync Settings")); } if (badge) { @@ -416,17 +429,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } - private get turningOnSync(): boolean { - return !!this.turningOnSyncContext.get(); - } - - private set turningOnSync(turningOn: boolean) { - this.turningOnSyncContext.set(turningOn); - this.updateGlobalActivityBadge(); - } - private async turnOn(): Promise { - this.turningOnSync = true; try { if (!this.storageService.getBoolean('sync.donotAskPreviewConfirmation', StorageScope.GLOBAL, false)) { if (!await this.askForConfirmation()) { @@ -447,14 +450,14 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo switch (e.code) { case UserDataSyncErrorCode.TooLarge: if (e.resource === SyncResource.Keybindings || e.resource === SyncResource.Settings) { - this.handleTooLargeError(e.resource, localize('too large while starting sync', "Preferences sync cannot be turned on because size of the {0} file to sync is larger than {1}. Please open the file and reduce the size and turn on sync", getSyncAreaLabel(e.resource).toLowerCase(), '100kb'), e); + this.handleTooLargeError(e.resource, localize('too large while starting sync', "Settings sync cannot be turned on because size of the {0} file to sync is larger than {1}. Please open the file and reduce the size and turn on sync", getSyncAreaLabel(e.resource).toLowerCase(), '100kb'), e); return; } break; case UserDataSyncErrorCode.IncompatibleLocalContent: case UserDataSyncErrorCode.Gone: case UserDataSyncErrorCode.UpgradeRequired: - const message = localize('error upgrade required while starting sync', "Preferences sync cannot be turned on because the current version ({0}, {1}) is not compatible with the sync service. Please update before turning on sync.", this.productService.version, this.productService.commit); + const message = localize('error upgrade required while starting sync', "Settings sync cannot be turned on because the current version ({0}, {1}) is not compatible with the sync service. Please update before turning on sync.", this.productService.version, this.productService.commit); const operationId = e.operationId ? localize('operationId', "Operation Id: {0}", e.operationId) : undefined; this.notificationService.notify({ severity: Severity.Error, @@ -464,10 +467,10 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo case UserDataSyncErrorCode.IncompatibleRemoteContent: this.notificationService.notify({ severity: Severity.Error, - message: localize('error reset required while starting sync', "Preferences sync cannot be turned on because your data in the cloud is older than that of in the client. Please reset your data in the cloud before turning on sync."), + message: localize('error reset required while starting sync', "Settings sync cannot be turned on because your data in the cloud is older than that of the client. Please clear your data in the cloud before turning on sync."), actions: { primary: [ - new Action('reset', localize('reset', "Reset Synced Data"), undefined, true, () => this.userDataSyncWorkbenchService.resetSyncedData()), + new Action('reset', localize('reset', "Clear Data in Cloud..."), undefined, true, () => this.userDataSyncWorkbenchService.resetSyncedData()), new Action('show synced data', localize('show synced data action', "Show Synced Data"), undefined, true, () => this.userDataSyncWorkbenchService.showSyncActivity()) ] } @@ -476,15 +479,13 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } this.notificationService.error(localize('turn on failed', "Error while starting Sync: {0}", toErrorMessage(e))); - } finally { - this.turningOnSync = false; } } private async askForConfirmation(): Promise { const result = await this.dialogService.show( Severity.Info, - localize('sync preview message', "Synchronizing your preferences is a preview feature, please read the documentation before turning it on."), + localize('sync preview message', "Synchronizing your settings is a preview feature, please read the documentation before turning it on."), [ localize('turn on', "Turn On"), localize('open doc', "Open Documentation"), @@ -506,7 +507,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo const disposables: DisposableStore = new DisposableStore(); const quickPick = this.quickInputService.createQuickPick(); disposables.add(quickPick); - quickPick.title = localize('Preferences Sync Title', "Preferences Sync"); + quickPick.title = SYNC_TITLE; quickPick.ok = false; quickPick.customButton = true; if (this.userDataSyncWorkbenchService.all.length) { @@ -552,7 +553,8 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo label: getSyncAreaLabel(SyncResource.Settings) }, { id: SyncResource.Keybindings, - label: getSyncAreaLabel(SyncResource.Keybindings) + label: getSyncAreaLabel(SyncResource.Keybindings), + description: this.configurationService.getValue('settingsSync.keybindingsPerPlatform') ? localize('per platform', "for each platform") : undefined }, { id: SyncResource.Snippets, label: getSyncAreaLabel(SyncResource.Snippets) @@ -580,7 +582,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo const disposables: DisposableStore = new DisposableStore(); const quickPick = this.quickInputService.createQuickPick(); disposables.add(quickPick); - quickPick.title = localize('configure sync', "Preferences Sync: Configure..."); + quickPick.title = localize('configure sync', "{0}: Configure...", SYNC_TITLE); quickPick.placeholder = localize('configure sync placeholder', "Choose what to sync"); quickPick.canSelectMany = true; quickPick.ignoreFocusOut = true; @@ -606,7 +608,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo const result = await this.dialogService.confirm({ type: 'info', message: localize('turn off sync confirmation', "Do you want to turn off sync?"), - detail: localize('turn off sync detail', "Your settings, keybindings, extensions and UI State will no longer be synced."), + detail: localize('turn off sync detail', "Your settings, keybindings, extensions, snippets and UI State will no longer be synced."), primaryButton: localize('turn off', "Turn Off"), checkbox: this.userDataSyncWorkbenchService.accountStatus === AccountStatus.Available ? { label: localize('turn off sync everywhere', "Turn off sync on all your devices and clear the data from the cloud.") @@ -650,18 +652,12 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private async handleConflicts([syncResource, conflicts]: [SyncResource, IResourcePreview[]]): Promise { for (const conflict of conflicts) { - let label: string | undefined = undefined; - if (syncResource === SyncResource.Settings) { - label = localize('settings conflicts preview', "Settings Conflicts (Remote ↔ Local)"); - } else if (syncResource === SyncResource.Keybindings) { - label = localize('keybindings conflicts preview', "Keybindings Conflicts (Remote ↔ Local)"); - } else if (syncResource === SyncResource.Snippets) { - label = localize('snippets conflicts preview', "User Snippet Conflicts (Remote ↔ Local) - {0}", basename(conflict.previewResource)); - } + const leftResourceName = localize({ key: 'leftResourceName', comment: ['remote as in file in cloud'] }, "{0} (Remote)", basename(conflict.remoteResource)); + const rightResourceName = localize('merges', "{0} (Merges)", basename(conflict.previewResource)); await this.editorService.openEditor({ leftResource: conflict.remoteResource, rightResource: conflict.previewResource, - label, + label: localize('sideBySideLabels', "{0} ↔ {1}", leftResourceName, rightResourceName), options: { preserveFocus: false, pinned: true, @@ -675,6 +671,54 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo return this.outputService.showChannel(Constants.userDataSyncLogChannelId); } + private async switchSyncService(): Promise { + const userDataSyncStore = this.userDataSyncStoreManagementService.userDataSyncStore; + if (userDataSyncStore?.insidersUrl && userDataSyncStore?.stableUrl && ![userDataSyncStore.insidersUrl, userDataSyncStore.stableUrl].includes(userDataSyncStore.url)) { + return new Promise((c, e) => { + const disposables: DisposableStore = new DisposableStore(); + const quickPick = disposables.add(this.quickInputService.createQuickPick<{ id: UserDataSyncStoreType, label: string, description?: string }>()); + quickPick.title = localize('switchSyncService.title', "Select Settings Sync Service..."); + quickPick.placeholder = localize('choose sync service', "Choose settings sync Service to use"); + quickPick.description = isNative ? + localize('choose sync service description', "Switching settings sync service requires restarting {0}", this.productService.nameLong) : + localize('choose sync service description web', "Switching settings sync service requires reloading {0}", this.productService.nameLong); + quickPick.hideInput = true; + const getDescription = (url: URI): string | undefined => { + const isCurrent = isEqual(url, userDataSyncStore.url); + const isDefault = isEqual(url, userDataSyncStore.defaultUrl); + if (isCurrent && isDefault) { + return localize('default and current', "Default & Current"); + } + if (isDefault) { + return localize('default', "Default"); + } + if (isCurrent) { + return localize('current', "Current"); + } + return undefined; + }; + quickPick.items = [ + { + id: 'insiders', + label: localize('insiders', "Insiders"), + description: getDescription(userDataSyncStore.insidersUrl!) + }, + { + id: 'stable', + label: localize('stable', "Stable"), + description: getDescription(userDataSyncStore.stableUrl!) + } + ]; + disposables.add(quickPick.onDidAccept(() => { + this.userDataSyncWorkbenchService.switchSyncService(quickPick.selectedItems[0].id); + quickPick.hide(); + })); + disposables.add(quickPick.onDidHide(() => disposables.dispose())); + quickPick.show(); + }); + } + } + private registerActions(): void { if (this.userDataAutoSyncService.canToggleEnablement()) { this.registerTurnOnSyncAction(); @@ -691,6 +735,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.registerSyncNowAction(); this.registerConfigureSyncAction(); this.registerShowSettingsAction(); + this.registerSwitchSyncServiceAction(); this.registerShowLogAction(); } @@ -701,7 +746,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo group: '5_sync', command: { id: turnOnSyncCommand.id, - title: localize('global activity turn on sync', "Turn on Preferences Sync...") + title: localize('global activity turn on sync', "Turn on Settings Sync...") }, when: turnOnSyncWhenContext, order: 1 @@ -714,7 +759,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo group: '5_sync', command: { id: turnOnSyncCommand.id, - title: localize('global activity turn on sync', "Turn on Preferences Sync...") + title: localize('global activity turn on sync', "Turn on Settings Sync...") }, when: turnOnSyncWhenContext, }); @@ -722,7 +767,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo group: '1_sync', command: { id: turnOnSyncCommand.id, - title: localize('global activity turn on sync', "Turn on Preferences Sync...") + title: localize('global activity turn on sync', "Turn on Settings Sync...") }, when: turnOnSyncWhenContext }); @@ -734,7 +779,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo constructor() { super({ id: 'workbench.userData.actions.turningOn', - title: localize('turnin on sync', "Turning on Preferences Sync..."), + title: localize('turnin on sync', "Turning on Settings Sync..."), precondition: ContextKeyExpr.false(), menu: [{ group: '5_sync', @@ -760,7 +805,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo constructor() { super({ id: 'workbench.userData.actions.signin', - title: localize('sign in global', "Sign in to Sync Preferences"), + title: localize('sign in global', "Sign in to Sync Settings"), menu: { group: '5_sync', id: MenuId.GlobalActivity, @@ -781,7 +826,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo group: '1_sync', command: { id, - title: localize('sign in accounts', "Sign in to Sync Preferences (1)"), + title: localize('sign in accounts', "Sign in to Sync Settings (1)"), }, when })); @@ -794,7 +839,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo group: '5_sync', command: { id: resolveSettingsConflictsCommand.id, - title: localize('resolveConflicts_global', "Preferences Sync: Show Settings Conflicts (1)"), + title: localize('resolveConflicts_global', "{0}: Show Settings Conflicts (1)", SYNC_TITLE), }, when: resolveSettingsConflictsWhenContext, order: 2 @@ -803,7 +848,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo group: '5_sync', command: { id: resolveSettingsConflictsCommand.id, - title: localize('resolveConflicts_global', "Preferences Sync: Show Settings Conflicts (1)"), + title: localize('resolveConflicts_global', "{0}: Show Settings Conflicts (1)", SYNC_TITLE), }, when: resolveSettingsConflictsWhenContext, order: 2 @@ -821,7 +866,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo group: '5_sync', command: { id: resolveKeybindingsConflictsCommand.id, - title: localize('resolveKeybindingsConflicts_global', "Preferences Sync: Show Keybindings Conflicts (1)"), + title: localize('resolveKeybindingsConflicts_global', "{0}: Show Keybindings Conflicts (1)", SYNC_TITLE), }, when: resolveKeybindingsConflictsWhenContext, order: 2 @@ -830,7 +875,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo group: '5_sync', command: { id: resolveKeybindingsConflictsCommand.id, - title: localize('resolveKeybindingsConflicts_global', "Preferences Sync: Show Keybindings Conflicts (1)"), + title: localize('resolveKeybindingsConflicts_global', "{0}: Show Keybindings Conflicts (1)", SYNC_TITLE), }, when: resolveKeybindingsConflictsWhenContext, order: 2 @@ -851,7 +896,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo group: '5_sync', command: { id: resolveSnippetsConflictsCommand.id, - title: localize('resolveSnippetsConflicts_global', "Preferences Sync: Show User Snippets Conflicts ({0})", conflicts?.length || 1), + title: localize('resolveSnippetsConflicts_global', "{0}: Show User Snippets Conflicts ({1})", SYNC_TITLE, conflicts?.length || 1), }, when: resolveSnippetsConflictsWhenContext, order: 2 @@ -860,7 +905,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo group: '5_sync', command: { id: resolveSnippetsConflictsCommand.id, - title: localize('resolveSnippetsConflicts_global', "Preferences Sync: Show User Snippets Conflicts ({0})", conflicts?.length || 1), + title: localize('resolveSnippetsConflicts_global', "{0}: Show User Snippets Conflicts ({1})", SYNC_TITLE, conflicts?.length || 1), }, when: resolveSnippetsConflictsWhenContext, order: 2 @@ -878,7 +923,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo constructor() { super({ id: 'workbench.userDataSync.actions.manage', - title: localize('sync is on', "Preferences Sync is On"), + title: localize('sync is on', "Settings Sync is On"), menu: [ { id: MenuId.GlobalActivity, @@ -952,13 +997,13 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private registerEnableSyncViewsAction(): void { const that = this; - const when = ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)); + const when = ContextKeyExpr.and(CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)); this._register(registerAction2(class SyncStatusAction extends Action2 { constructor() { super({ id: showSyncedDataCommand.id, title: { value: localize('workbench.action.showSyncRemoteBackup', "Show Synced Data"), original: `Show Synced Data` }, - category: { value: localize('sync preferences', "Preferences Sync"), original: `Preferences Sync` }, + category: { value: SYNC_TITLE, original: `Settings Sync` }, precondition: when, menu: { id: MenuId.CommandPalette, @@ -1040,7 +1085,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo constructor() { super({ id: SHOW_SYNC_LOG_COMMAND_ID, - title: localize('show sync log title', "Preferences Sync: Show Log"), + title: localize('show sync log title', "{0}: Show Log", SYNC_TITLE), menu: { id: MenuId.CommandPalette, when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)), @@ -1069,6 +1114,28 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo })); } + private registerSwitchSyncServiceAction(): void { + const that = this; + const userDataSyncStore = this.userDataSyncStoreManagementService.userDataSyncStore; + if (userDataSyncStore?.insidersUrl && userDataSyncStore?.stableUrl && ![userDataSyncStore.insidersUrl, userDataSyncStore.stableUrl].includes(userDataSyncStore.url)) { + this._register(registerAction2(class ShowSyncSettingsAction extends Action2 { + constructor() { + super({ + id: 'workbench.userDataSync.actions.switchSyncService', + title: { value: localize('workbench.userDataSync.actions.switchSyncService', "{0}: Select Service...", SYNC_TITLE), original: 'Settings Sync: Select Service...' }, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)), + }, + }); + } + run(accessor: ServicesAccessor): any { + return that.switchSyncService(); + } + })); + } + } + private registerViews(): void { const container = this.registerViewContainer(); this.registerDataViews(container); @@ -1078,7 +1145,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo return Registry.as(Extensions.ViewContainersRegistry).registerViewContainer( { id: SYNC_VIEW_CONTAINER_ID, - name: localize('sync preferences', "Preferences Sync"), + name: SYNC_TITLE, ctorDescriptor: new SyncDescriptor( UserDataSyncViewPaneContainer, [SYNC_VIEW_CONTAINER_ID] @@ -1130,7 +1197,6 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio @IConfigurationService private readonly configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, - @IUserDataSyncWorkbenchService private readonly userDataSyncWorkbenchService: IUserDataSyncWorkbenchService, ) { super(); @@ -1159,6 +1225,10 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio return false; // we need a model } + if (!this.userDataAutoSyncService.isEnabled()) { + return false; + } + const syncResourceConflicts = this.getSyncResourceConflicts(model.uri); if (!syncResourceConflicts) { return false; @@ -1181,40 +1251,36 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio const [syncResource, conflicts] = this.getSyncResourceConflicts(resource)!; const isRemote = conflicts.some(({ remoteResource }) => isEqual(remoteResource, resource)); const acceptRemoteLabel = localize('accept remote', "Accept Remote"); - const acceptLocalLabel = localize('accept merge preview', "Accept Merge Preview"); - this.acceptChangesButton = this.instantiationService.createInstance(FloatingClickWidget, this.editor, isRemote ? acceptRemoteLabel : acceptLocalLabel, null); + const acceptMergesLabel = localize('accept merges', "Accept Merges"); + this.acceptChangesButton = this.instantiationService.createInstance(FloatingClickWidget, this.editor, isRemote ? acceptRemoteLabel : acceptMergesLabel, null); this._register(this.acceptChangesButton.onClick(async () => { const model = this.editor.getModel(); if (model) { this.telemetryService.publicLog2<{ source: string, action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: syncResource, action: isRemote ? 'acceptRemote' : 'acceptLocal' }); const syncAreaLabel = getSyncAreaLabel(syncResource); - if (this.userDataAutoSyncService.isEnabled()) { - const result = await this.dialogService.confirm({ - type: 'info', - title: isRemote - ? localize('Sync accept remote', "Preferences Sync: {0}", acceptRemoteLabel) - : localize('Sync accept local', "Preferences Sync: {0}", acceptLocalLabel), - message: isRemote - ? localize('confirm replace and overwrite local', "Would you like to accept remote {0} and replace local {1}?", syncAreaLabel.toLowerCase(), syncAreaLabel.toLowerCase()) - : localize('confirm replace and overwrite remote', "Would you like to accept local {0} and replace remote {1}?", syncAreaLabel.toLowerCase(), syncAreaLabel.toLowerCase()), - primaryButton: isRemote ? acceptRemoteLabel : acceptLocalLabel - }); - if (result.confirmed) { - try { - await this.userDataSyncService.accept(syncResource, model.uri, model.getValue(), true); - } catch (e) { - if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.LocalPreconditionFailed) { - const syncResourceCoflicts = this.userDataSyncService.conflicts.filter(syncResourceCoflicts => syncResourceCoflicts[0] === syncResource)[0]; - if (syncResourceCoflicts && conflicts.some(conflict => isEqual(conflict.previewResource, model.uri) || isEqual(conflict.remoteResource, model.uri))) { - this.notificationService.warn(localize('update conflicts', "Could not resolve conflicts as there is new local version available. Please try again.")); - } - } else { - this.notificationService.error(e); + const result = await this.dialogService.confirm({ + type: 'info', + title: isRemote + ? localize('Sync accept remote', "{0}: {1}", SYNC_TITLE, acceptRemoteLabel) + : localize('Sync accept merges', "{0}: {1}", SYNC_TITLE, acceptMergesLabel), + message: isRemote + ? localize('confirm replace and overwrite local', "Would you like to accept remote {0} and replace local {1}?", syncAreaLabel.toLowerCase(), syncAreaLabel.toLowerCase()) + : localize('confirm replace and overwrite remote', "Would you like to accept merges and replace remote {0}?", syncAreaLabel.toLowerCase()), + primaryButton: isRemote ? acceptRemoteLabel : acceptMergesLabel + }); + if (result.confirmed) { + try { + await this.userDataSyncService.accept(syncResource, model.uri, model.getValue(), true); + } catch (e) { + if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.LocalPreconditionFailed) { + const syncResourceCoflicts = this.userDataSyncService.conflicts.filter(syncResourceCoflicts => syncResourceCoflicts[0] === syncResource)[0]; + if (syncResourceCoflicts && conflicts.some(conflict => isEqual(conflict.previewResource, model.uri) || isEqual(conflict.remoteResource, model.uri))) { + this.notificationService.warn(localize('update conflicts', "Could not resolve conflicts as there is new local version available. Please try again.")); } + } else { + this.notificationService.error(e); } } - } else { - await this.userDataSyncWorkbenchService.userDataSyncPreview.accept(syncResource, model.uri, model.getValue()); } } })); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataManualSyncView.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncMergesView.ts similarity index 65% rename from src/vs/workbench/contrib/userDataSync/browser/userDataManualSyncView.ts rename to src/vs/workbench/contrib/userDataSync/browser/userDataSyncMergesView.ts index 4d8c78b3534..bf0675cdb11 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataManualSyncView.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncMergesView.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/userDataSyncViews'; -import { ITreeViewDataProvider, ITreeItem, TreeItemCollapsibleState, TreeViewItemHandleArg, IViewDescriptorService } from 'vs/workbench/common/views'; +import { ITreeItem, TreeItemCollapsibleState, TreeViewItemHandleArg, IViewDescriptorService } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; import { TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -13,10 +13,10 @@ import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/act import { ContextKeyExpr, ContextKeyEqualsExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { URI } from 'vs/base/common/uri'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, dispose } from 'vs/base/common/lifecycle'; import { Codicon } from 'vs/base/common/codicons'; -import { IUserDataSyncWorkbenchService, getSyncAreaLabel, IUserDataSyncPreview, IUserDataSyncResource, MANUAL_SYNC_VIEW_ID } from 'vs/workbench/services/userDataSync/common/userDataSync'; +import { IUserDataSyncWorkbenchService, getSyncAreaLabel, IUserDataSyncPreview, IUserDataSyncResource, SYNC_MERGES_VIEW_ID } from 'vs/workbench/services/userDataSync/common/userDataSync'; import { isEqual, basename } from 'vs/base/common/resources'; import { IDecorationsProvider, IDecorationData, IDecorationsService } from 'vs/workbench/services/decorations/browser/decorations'; import { IProgressService } from 'vs/platform/progress/common/progress'; @@ -32,8 +32,14 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { attachButtonStyler } from 'vs/platform/theme/common/styler'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { Severity } from 'vs/platform/notification/common/notification'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -export class UserDataManualSyncViewPane extends TreeViewPane { +export class UserDataSyncMergesViewPane extends TreeViewPane { private userDataSyncPreview: IUserDataSyncPreview; @@ -41,11 +47,13 @@ export class UserDataManualSyncViewPane extends TreeViewPane { private syncButton!: Button; private cancelButton!: Button; + private readonly treeItems = new Map(); + constructor( options: IViewletViewOptions, @IEditorService private readonly editorService: IEditorService, + @IDialogService private readonly dialogService: IDialogService, @IProgressService private readonly progressService: IProgressService, - @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @IUserDataSyncWorkbenchService userDataSyncWorkbenchService: IUserDataSyncWorkbenchService, @IDecorationsService decorationsService: IDecorationsService, @IKeybindingService keybindingService: IKeybindingService, @@ -63,7 +71,7 @@ export class UserDataManualSyncViewPane extends TreeViewPane { this._register(this.userDataSyncPreview.onDidChangeResources(() => this.updateSyncButtonEnablement())); this._register(this.userDataSyncPreview.onDidChangeResources(() => this.treeView.refresh())); - this._register(this.userDataSyncPreview.onDidChangeResources(() => this.closeConflictEditors())); + this._register(this.userDataSyncPreview.onDidChangeResources(() => this.closeDiffEditors())); this._register(decorationsService.registerDecorationsProvider(this._register(new UserDataSyncResourcesDecorationProvider(this.userDataSyncPreview)))); this.registerActions(); @@ -71,11 +79,18 @@ export class UserDataManualSyncViewPane extends TreeViewPane { protected renderTreeView(container: HTMLElement): void { super.renderTreeView(DOM.append(container, DOM.$(''))); + this.createButtons(container); + const that = this; + this.treeView.message = localize('explanation', "Please go through each entry and merge to enable sync."); + this.treeView.dataProvider = { getChildren() { return that.getTreeItems(); } }; + } + + private createButtons(container: HTMLElement): void { this.buttonsContainer = DOM.append(container, DOM.$('.manual-sync-buttons-container')); this.syncButton = this._register(new Button(this.buttonsContainer)); - this.syncButton.label = localize('turn on sync', "Turn on Preferences Sync"); + this.syncButton.label = localize('turn on sync', "Turn on Settings Sync"); this.updateSyncButtonEnablement(); this._register(attachButtonStyler(this.syncButton, this.themeService)); this._register(this.syncButton.onDidClick(() => this.apply())); @@ -84,22 +99,58 @@ export class UserDataManualSyncViewPane extends TreeViewPane { this.cancelButton.label = localize('cancel', "Cancel"); this._register(attachButtonStyler(this.cancelButton, this.themeService)); this._register(this.cancelButton.onDidClick(() => this.cancel())); - - this.treeView.dataProvider = new ManualSyncViewDataProvider(this.userDataSyncPreview); } protected layoutTreeView(height: number, width: number): void { - const buttonContainerHeight = 117; + const buttonContainerHeight = 78; this.buttonsContainer.style.height = `${buttonContainerHeight}px`; this.buttonsContainer.style.width = `${width}px`; + const numberOfChanges = this.userDataSyncPreview.resources.filter(r => r.syncResource !== SyncResource.GlobalState && (r.localChange !== Change.None || r.remoteChange !== Change.None)).length; - super.layoutTreeView(Math.min(height - buttonContainerHeight, 22 * numberOfChanges), width); + const messageHeight = 44; + super.layoutTreeView(Math.min(height - buttonContainerHeight, ((22 * numberOfChanges) + messageHeight)), width); } private updateSyncButtonEnablement(): void { this.syncButton.enabled = this.userDataSyncPreview.resources.every(c => c.syncResource === SyncResource.GlobalState || c.mergeState === MergeState.Accepted); } + private async getTreeItems(): Promise { + this.treeItems.clear(); + const roots: ITreeItem[] = []; + for (const resource of this.userDataSyncPreview.resources) { + if (resource.syncResource !== SyncResource.GlobalState && (resource.localChange !== Change.None || resource.remoteChange !== Change.None)) { + const handle = JSON.stringify(resource); + const treeItem = { + handle, + resourceUri: resource.remote, + label: { label: basename(resource.remote), strikethrough: resource.mergeState === MergeState.Accepted && (resource.localChange === Change.Deleted || resource.remoteChange === Change.Deleted) }, + description: getSyncAreaLabel(resource.syncResource), + collapsibleState: TreeItemCollapsibleState.None, + command: { id: `workbench.actions.sync.showChanges`, title: '', arguments: [{ $treeViewId: '', $treeItemHandle: handle }] }, + contextValue: `sync-resource-${resource.mergeState}` + }; + this.treeItems.set(handle, treeItem); + roots.push(treeItem); + } + } + return roots; + } + + private toUserDataSyncResourceGroup(handle: string): IUserDataSyncResource { + const parsed: IUserDataSyncResource = JSON.parse(handle); + return { + syncResource: parsed.syncResource, + local: URI.revive(parsed.local), + remote: URI.revive(parsed.remote), + merged: URI.revive(parsed.merged), + accepted: URI.revive(parsed.accepted), + localChange: parsed.localChange, + remoteChange: parsed.remoteChange, + mergeState: parsed.mergeState, + }; + } + private registerActions(): void { const that = this; @@ -112,14 +163,14 @@ export class UserDataManualSyncViewPane extends TreeViewPane { icon: Codicon.cloudDownload, menu: { id: MenuId.ViewItemContext, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', MANUAL_SYNC_VIEW_ID), ContextKeyExpr.equals('viewItem', 'sync-resource-preview')), + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', SYNC_MERGES_VIEW_ID), ContextKeyExpr.equals('viewItem', 'sync-resource-preview')), group: 'inline', order: 1, }, }); } async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise { - return that.acceptRemote(ManualSyncViewDataProvider.toUserDataSyncResourceGroup(handle.$treeItemHandle)); + return that.acceptRemote(that.toUserDataSyncResourceGroup(handle.$treeItemHandle)); } })); @@ -132,14 +183,14 @@ export class UserDataManualSyncViewPane extends TreeViewPane { icon: Codicon.cloudUpload, menu: { id: MenuId.ViewItemContext, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', MANUAL_SYNC_VIEW_ID), ContextKeyExpr.equals('viewItem', 'sync-resource-preview')), + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', SYNC_MERGES_VIEW_ID), ContextKeyExpr.equals('viewItem', 'sync-resource-preview')), group: 'inline', order: 2, }, }); } async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise { - return that.acceptLocal(ManualSyncViewDataProvider.toUserDataSyncResourceGroup(handle.$treeItemHandle)); + return that.acceptLocal(that.toUserDataSyncResourceGroup(handle.$treeItemHandle)); } })); @@ -149,17 +200,17 @@ export class UserDataManualSyncViewPane extends TreeViewPane { super({ id: `workbench.actions.sync.merge`, title: localize('workbench.actions.sync.merge', "Merge"), - icon: Codicon.gitMerge, + icon: Codicon.merge, menu: { id: MenuId.ViewItemContext, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', MANUAL_SYNC_VIEW_ID), ContextKeyExpr.equals('viewItem', 'sync-resource-preview')), + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', SYNC_MERGES_VIEW_ID), ContextKeyExpr.equals('viewItem', 'sync-resource-preview')), group: 'inline', order: 3, }, }); } async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise { - return that.mergeResource(ManualSyncViewDataProvider.toUserDataSyncResourceGroup(handle.$treeItemHandle)); + return that.mergeResource(that.toUserDataSyncResourceGroup(handle.$treeItemHandle)); } })); @@ -172,14 +223,14 @@ export class UserDataManualSyncViewPane extends TreeViewPane { icon: Codicon.discard, menu: { id: MenuId.ViewItemContext, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', MANUAL_SYNC_VIEW_ID), ContextKeyExpr.or(ContextKeyExpr.equals('viewItem', 'sync-resource-accepted'), ContextKeyExpr.equals('viewItem', 'sync-resource-conflict'))), + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', SYNC_MERGES_VIEW_ID), ContextKeyExpr.or(ContextKeyExpr.equals('viewItem', 'sync-resource-accepted'), ContextKeyExpr.equals('viewItem', 'sync-resource-conflict'))), group: 'inline', order: 3, }, }); } async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise { - return that.discardResource(ManualSyncViewDataProvider.toUserDataSyncResourceGroup(handle.$treeItemHandle)); + return that.discardResource(that.toUserDataSyncResourceGroup(handle.$treeItemHandle)); } })); @@ -191,7 +242,7 @@ export class UserDataManualSyncViewPane extends TreeViewPane { }); } async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise { - const previewResource: IUserDataSyncResource = ManualSyncViewDataProvider.toUserDataSyncResourceGroup(handle.$treeItemHandle); + const previewResource: IUserDataSyncResource = that.toUserDataSyncResourceGroup(handle.$treeItemHandle); return that.open(previewResource); } })); @@ -199,23 +250,27 @@ export class UserDataManualSyncViewPane extends TreeViewPane { private async acceptLocal(userDataSyncResource: IUserDataSyncResource): Promise { await this.withProgress(async () => { - const content = await this.userDataSyncService.resolveContent(userDataSyncResource.local); - await this.userDataSyncPreview.accept(userDataSyncResource.syncResource, userDataSyncResource.local, content || ''); + await this.userDataSyncPreview.accept(userDataSyncResource.syncResource, userDataSyncResource.local); }); await this.reopen(userDataSyncResource); } private async acceptRemote(userDataSyncResource: IUserDataSyncResource): Promise { await this.withProgress(async () => { - const content = await this.userDataSyncService.resolveContent(userDataSyncResource.remote); - await this.userDataSyncPreview.accept(userDataSyncResource.syncResource, userDataSyncResource.remote, content || ''); + await this.userDataSyncPreview.accept(userDataSyncResource.syncResource, userDataSyncResource.remote); }); await this.reopen(userDataSyncResource); } private async mergeResource(previewResource: IUserDataSyncResource): Promise { await this.withProgress(() => this.userDataSyncPreview.merge(previewResource.merged)); + previewResource = this.userDataSyncPreview.resources.find(({ local }) => isEqual(local, previewResource.local))!; await this.reopen(previewResource); + if (previewResource.mergeState === MergeState.Conflict) { + await this.dialogService.show(Severity.Warning, localize('conflicts detected', "Conflicts Detected"), [], { + detail: localize('resolve', "Unable to merge due to conflicts. Please resolve them to continue.") + }); + } } private async discardResource(previewResource: IUserDataSyncResource): Promise { @@ -224,19 +279,16 @@ export class UserDataManualSyncViewPane extends TreeViewPane { } private async apply(): Promise { + this.closeAll(); this.syncButton.label = localize('turning on', "Turning on..."); this.syncButton.enabled = false; this.cancelButton.enabled = false; - return this.withProgress(async () => { - for (const resource of this.userDataSyncPreview.resources) { - if (resource.syncResource === SyncResource.GlobalState) { - await this.userDataSyncPreview.merge(resource.merged); - } else { - this.close(resource); - } - } - await this.userDataSyncPreview.apply(); - }); + try { + await this.withProgress(async () => this.userDataSyncPreview.apply()); + } catch (error) { + this.syncButton.enabled = false; + this.cancelButton.enabled = true; + } } private async cancel(): Promise { @@ -256,7 +308,7 @@ export class UserDataManualSyncViewPane extends TreeViewPane { const leftResource = previewResource.remote; const rightResource = previewResource.mergeState === MergeState.Conflict ? previewResource.merged : previewResource.local; const leftResourceName = localize({ key: 'leftResourceName', comment: ['remote as in file in cloud'] }, "{0} (Remote)", basename(leftResource)); - const rightResourceName = previewResource.mergeState === MergeState.Conflict ? localize('merge preview', "{0} (Merge Preview)", basename(rightResource)) + const rightResourceName = previewResource.mergeState === MergeState.Conflict ? localize('merges', "{0} (Merges)", basename(rightResource)) : localize({ key: 'rightResourceName', comment: ['local as in file in disk'] }, "{0} (Local)", basename(rightResource)); await this.editorService.openEditor({ leftResource, @@ -274,11 +326,15 @@ export class UserDataManualSyncViewPane extends TreeViewPane { this.close(previewResource); const resource = this.userDataSyncPreview.resources.find(({ local }) => isEqual(local, previewResource.local)); if (resource) { + // select the resource + await this.treeView.refresh(); + this.treeView.setSelection([this.treeItems.get(JSON.stringify(resource))!]); + await this.open(resource); } } - private close(previewResource: IUserDataSyncResource) { + private close(previewResource: IUserDataSyncResource): void { for (const input of this.editorService.editors) { if (input instanceof DiffEditorInput) { // Close all diff editors @@ -293,12 +349,13 @@ export class UserDataManualSyncViewPane extends TreeViewPane { } } - private closeConflictEditors() { + private closeDiffEditors() { for (const previewResource of this.userDataSyncPreview.resources) { - if (previewResource.mergeState !== MergeState.Conflict) { + if (previewResource.mergeState === MergeState.Accepted) { for (const input of this.editorService.editors) { if (input instanceof DiffEditorInput) { - if (isEqual(previewResource.remote, input.secondary.resource) && isEqual(previewResource.merged, input.primary.resource)) { + if (isEqual(previewResource.remote, input.secondary.resource) && + (isEqual(previewResource.merged, input.primary.resource) || isEqual(previewResource.local, input.primary.resource))) { input.dispose(); } } @@ -307,50 +364,14 @@ export class UserDataManualSyncViewPane extends TreeViewPane { } } - private withProgress(task: () => Promise): Promise { - return this.progressService.withProgress({ location: MANUAL_SYNC_VIEW_ID, delay: 500 }, task); - } - -} - -class ManualSyncViewDataProvider implements ITreeViewDataProvider { - - constructor( - private readonly userDataSyncPreview: IUserDataSyncPreview - ) { - } - - async getChildren(): Promise { - const roots: ITreeItem[] = []; - for (const resource of this.userDataSyncPreview.resources) { - if (resource.syncResource !== SyncResource.GlobalState && (resource.localChange !== Change.None || resource.remoteChange !== Change.None)) { - const handle = JSON.stringify(resource); - roots.push({ - handle, - resourceUri: resource.remote, - label: { label: basename(resource.remote), strikethrough: resource.mergeState === MergeState.Accepted && (resource.localChange === Change.Deleted || resource.remoteChange === Change.Deleted) }, - description: getSyncAreaLabel(resource.syncResource), - collapsibleState: TreeItemCollapsibleState.None, - command: { id: `workbench.actions.sync.showChanges`, title: '', arguments: [{ $treeViewId: '', $treeItemHandle: handle }] }, - contextValue: `sync-resource-${resource.mergeState}` - }); - } + private closeAll() { + for (const previewResource of this.userDataSyncPreview.resources) { + this.close(previewResource); } - return roots; } - static toUserDataSyncResourceGroup(handle: string): IUserDataSyncResource { - const parsed: IUserDataSyncResource = JSON.parse(handle); - return { - syncResource: parsed.syncResource, - local: URI.revive(parsed.local), - remote: URI.revive(parsed.remote), - merged: URI.revive(parsed.merged), - accepted: URI.revive(parsed.accepted), - localChange: parsed.localChange, - remoteChange: parsed.remoteChange, - mergeState: parsed.mergeState, - }; + private withProgress(task: () => Promise): Promise { + return this.progressService.withProgress({ location: SYNC_MERGES_VIEW_ID, delay: 500 }, task); } } @@ -380,3 +401,102 @@ class UserDataSyncResourcesDecorationProvider extends Disposable implements IDec return undefined; } } + +type AcceptChangesClassification = { + source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; + action: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; +}; + +class AcceptChangesContribution extends Disposable implements IEditorContribution { + + static get(editor: ICodeEditor): AcceptChangesContribution { + return editor.getContribution(AcceptChangesContribution.ID); + } + + public static readonly ID = 'editor.contrib.acceptChangesButton'; + + private acceptChangesButton: FloatingClickWidget | undefined; + + constructor( + private editor: ICodeEditor, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IUserDataSyncWorkbenchService private readonly userDataSyncWorkbenchService: IUserDataSyncWorkbenchService, + ) { + super(); + + this.update(); + this.registerListeners(); + } + + private registerListeners(): void { + this._register(this.editor.onDidChangeModel(() => this.update())); + this._register(this.userDataSyncService.onDidChangeConflicts(() => this.update())); + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('diffEditor.renderSideBySide'))(() => this.update())); + } + + private update(): void { + if (!this.shouldShowButton(this.editor)) { + this.disposeAcceptChangesWidgetRenderer(); + return; + } + + this.createAcceptChangesWidgetRenderer(); + } + + private shouldShowButton(editor: ICodeEditor): boolean { + const model = editor.getModel(); + if (!model) { + return false; // we need a model + } + + const userDataSyncResource = this.getUserDataSyncResource(model.uri); + if (!userDataSyncResource) { + return false; + } + + return true; + } + + private createAcceptChangesWidgetRenderer(): void { + if (!this.acceptChangesButton) { + const resource = this.editor.getModel()!.uri; + const userDataSyncResource = this.getUserDataSyncResource(resource)!; + + const isRemoteResource = isEqual(userDataSyncResource.remote, resource); + const isLocalResource = isEqual(userDataSyncResource.local, resource); + const label = isRemoteResource ? localize('accept remote', "Accept Remote") + : isLocalResource ? localize('accept local', "Accept Local") + : localize('accept merges', "Accept Merges"); + + this.acceptChangesButton = this.instantiationService.createInstance(FloatingClickWidget, this.editor, label, null); + this._register(this.acceptChangesButton.onClick(async () => { + const model = this.editor.getModel(); + if (model) { + this.telemetryService.publicLog2<{ source: string, action: string }, AcceptChangesClassification>('sync/acceptChanges', { source: userDataSyncResource.syncResource, action: isRemoteResource ? 'acceptRemote' : isLocalResource ? 'acceptLocal' : 'acceptMerges' }); + await this.userDataSyncWorkbenchService.userDataSyncPreview.accept(userDataSyncResource.syncResource, model.uri, model.getValue()); + } + })); + + this.acceptChangesButton.render(); + } + } + + private getUserDataSyncResource(resource: URI): IUserDataSyncResource | undefined { + return this.userDataSyncWorkbenchService.userDataSyncPreview.resources.find(r => isEqual(resource, r.local) || isEqual(resource, r.remote) || isEqual(resource, r.merged)); + } + + private disposeAcceptChangesWidgetRenderer(): void { + dispose(this.acceptChangesButton); + this.acceptChangesButton = undefined; + } + + dispose(): void { + this.disposeAcceptChangesWidgetRenderer(); + super.dispose(); + } +} + +registerEditorContribution(AcceptChangesContribution.ID, AcceptChangesContribution); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts index c4a1c631cbb..a3434826dd5 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts @@ -30,13 +30,14 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IAction, Action } from 'vs/base/common/actions'; -import { IUserDataSyncWorkbenchService, CONTEXT_SYNC_STATE, getSyncAreaLabel, CONTEXT_ACCOUNT_STATE, AccountStatus, CONTEXT_ENABLE_ACTIVITY_VIEWS, SHOW_SYNC_LOG_COMMAND_ID, CONFIGURE_SYNC_COMMAND_ID, MANUAL_SYNC_VIEW_ID, CONTEXT_ENABLE_MANUAL_SYNC_VIEW } from 'vs/workbench/services/userDataSync/common/userDataSync'; +import { IUserDataSyncWorkbenchService, CONTEXT_SYNC_STATE, getSyncAreaLabel, CONTEXT_ACCOUNT_STATE, AccountStatus, CONTEXT_ENABLE_ACTIVITY_VIEWS, SHOW_SYNC_LOG_COMMAND_ID, CONFIGURE_SYNC_COMMAND_ID, SYNC_MERGES_VIEW_ID, CONTEXT_ENABLE_SYNC_MERGES_VIEW, SYNC_TITLE } from 'vs/workbench/services/userDataSync/common/userDataSync'; import { IUserDataSyncMachinesService, IUserDataSyncMachine } from 'vs/platform/userDataSync/common/userDataSyncMachines'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { TreeView } from 'vs/workbench/contrib/views/browser/treeView'; import { flatten } from 'vs/base/common/arrays'; -import { UserDataManualSyncViewPane } from 'vs/workbench/contrib/userDataSync/browser/userDataManualSyncView'; +import { UserDataSyncMergesViewPane } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncMergesView'; +import { basename } from 'vs/base/common/resources'; export class UserDataSyncViewPaneContainer extends ViewPaneContainer { @@ -55,7 +56,7 @@ export class UserDataSyncViewPaneContainer extends ViewPaneContainer { @IExtensionService extensionService: IExtensionService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, ) { - super(containerId, { mergeViewWithContainerWhenSingleView: false }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); + super(containerId, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); } getActions(): IAction[] { @@ -67,7 +68,7 @@ export class UserDataSyncViewPaneContainer extends ViewPaneContainer { getSecondaryActions(): IAction[] { return [ - new Action('workbench.actions.syncData.reset', localize('workbench.actions.syncData.reset', "Reset Synced Data"), undefined, true, () => this.userDataSyncWorkbenchService.resetSyncedData()), + new Action('workbench.actions.syncData.reset', localize('workbench.actions.syncData.reset', "Clear Data in Cloud..."), undefined, true, () => this.userDataSyncWorkbenchService.resetSyncedData()), ]; } @@ -80,13 +81,15 @@ export class UserDataSyncDataViews extends Disposable { @IInstantiationService private readonly instantiationService: IInstantiationService, @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, @IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, + @IUserDataSyncMachinesService private readonly userDataSyncMachinesService: IUserDataSyncMachinesService, + @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, ) { super(); this.registerViews(container); } private registerViews(container: ViewContainer): void { - this.registerManualSyncView(container); + this.registerMergesView(container); this.registerActivityView(container, true); this.registerMachinesView(container); @@ -94,17 +97,17 @@ export class UserDataSyncDataViews extends Disposable { this.registerActivityView(container, false); } - private registerManualSyncView(container: ViewContainer): void { + private registerMergesView(container: ViewContainer): void { const viewsRegistry = Registry.as(Extensions.ViewsRegistry); - const viewName = localize('manual sync', "Manual Sync"); + const viewName = localize('merges', "Merges"); viewsRegistry.registerViews([{ - id: MANUAL_SYNC_VIEW_ID, + id: SYNC_MERGES_VIEW_ID, name: viewName, - ctorDescriptor: new SyncDescriptor(UserDataManualSyncViewPane), - when: CONTEXT_ENABLE_MANUAL_SYNC_VIEW, + ctorDescriptor: new SyncDescriptor(UserDataSyncMergesViewPane), + when: CONTEXT_ENABLE_SYNC_MERGES_VIEW, canToggleVisibility: false, canMoveView: false, - treeView: this.instantiationService.createInstance(TreeView, MANUAL_SYNC_VIEW_ID, viewName), + treeView: this.instantiationService.createInstance(TreeView, SYNC_MERGES_VIEW_ID, viewName), collapsed: false, order: 100, }], container); @@ -122,7 +125,7 @@ export class UserDataSyncDataViews extends Disposable { treeView.dataProvider = dataProvider; } }); - this._register(Event.any(this.userDataSyncResourceEnablementService.onDidChangeResourceEnablement, this.userDataAutoSyncService.onDidChangeEnablement)(() => treeView.refresh())); + this._register(Event.any(this.userDataSyncMachinesService.onDidChange, this.userDataSyncService.onDidResetRemote)(() => treeView.refresh())); const viewsRegistry = Registry.as(Extensions.ViewsRegistry); viewsRegistry.registerViews([{ id, @@ -161,7 +164,7 @@ export class UserDataSyncDataViews extends Disposable { constructor() { super({ id: `workbench.actions.sync.turnOffSyncOnMachine`, - title: localize('workbench.actions.sync.turnOffSyncOnMachine', "Turn off Preferences Sync"), + title: localize('workbench.actions.sync.turnOffSyncOnMachine', "Turn off Settings Sync"), menu: { id: MenuId.ViewItemContext, when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', id), ContextKeyEqualsExpr.create('viewItem', 'sync-machine')), @@ -190,7 +193,10 @@ export class UserDataSyncDataViews extends Disposable { : this.instantiationService.createInstance(LocalUserDataSyncActivityViewDataProvider); } }); - this._register(Event.any(this.userDataSyncResourceEnablementService.onDidChangeResourceEnablement, this.userDataAutoSyncService.onDidChangeEnablement)(() => treeView.refresh())); + this._register(Event.any(this.userDataSyncResourceEnablementService.onDidChangeResourceEnablement, + this.userDataAutoSyncService.onDidChangeEnablement, + this.userDataSyncService.onDidResetLocal, + this.userDataSyncService.onDidResetRemote)(() => treeView.refresh())); const viewsRegistry = Registry.as(Extensions.ViewsRegistry); viewsRegistry.registerViews([{ id, @@ -247,7 +253,7 @@ export class UserDataSyncDataViews extends Disposable { const result = await dialogService.confirm({ message: localize({ key: 'confirm replace', comment: ['A confirmation message to replace current user data (settings, extensions, keybindings, snippets) with selected version'] }, "Would you like to replace your current {0} with selected?", getSyncAreaLabel(syncResource)), type: 'info', - title: localize('preferences sync', "Preferences Sync") + title: SYNC_TITLE }); if (result.confirmed) { return userDataSyncService.replace(URI.parse(resource)); @@ -264,19 +270,20 @@ export class UserDataSyncDataViews extends Disposable { } async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise { const editorService = accessor.get(IEditorService); - const { resource, comparableResource } = <{ resource: string, comparableResource?: string }>JSON.parse(handle.$treeItemHandle); - if (comparableResource) { - await editorService.openEditor({ - leftResource: URI.parse(resource), - rightResource: URI.parse(comparableResource), - options: { - preserveFocus: true, - revealIfVisible: true, - }, - }); - } else { - await editorService.openEditor({ resource: URI.parse(resource) }); - } + const { resource, comparableResource } = <{ resource: string, comparableResource: string }>JSON.parse(handle.$treeItemHandle); + const leftResource = URI.parse(resource); + const leftResourceName = localize({ key: 'leftResourceName', comment: ['remote as in file in cloud'] }, "{0} (Remote)", basename(leftResource)); + const rightResource = URI.parse(comparableResource); + const rightResourceName = localize({ key: 'rightResourceName', comment: ['local as in file in disk'] }, "{0} (Local)", basename(rightResource)); + await editorService.openEditor({ + leftResource, + rightResource, + label: localize('sideBySideLabels', "{0} ↔ {1}", leftResourceName, rightResourceName), + options: { + preserveFocus: true, + revealIfVisible: true, + }, + }); } }); } @@ -354,7 +361,7 @@ abstract class UserDataSyncActivityViewDataProvider implements ITreeViewDataProv protected async getChildrenForSyncResourceTreeItem(element: SyncResourceHandleTreeItem): Promise { const associatedResources = await this.userDataSyncService.getAssociatedResources((element).syncResourceHandle.syncResource, (element).syncResourceHandle); return associatedResources.map(({ resource, comparableResource }) => { - const handle = JSON.stringify({ resource: resource.toString(), comparableResource: comparableResource?.toString() }); + const handle = JSON.stringify({ resource: resource.toString(), comparableResource: comparableResource.toString() }); return { handle, collapsibleState: TreeItemCollapsibleState.None, @@ -419,11 +426,13 @@ class RemoteUserDataSyncActivityViewDataProvider extends UserDataSyncActivityVie protected async getChildrenForSyncResourceTreeItem(element: SyncResourceHandleTreeItem): Promise { const children = await super.getChildrenForSyncResourceTreeItem(element); - const machineId = await this.userDataSyncService.getMachineId(element.syncResourceHandle.syncResource, element.syncResourceHandle); - if (machineId) { - const machines = await this.getMachines(); - const machine = machines.find(({ id }) => id === machineId); - children[0].description = machine?.isCurrent ? localize({ key: 'current', comment: ['Represents current machine'] }, "Current") : machine?.name; + if (children.length) { + const machineId = await this.userDataSyncService.getMachineId(element.syncResourceHandle.syncResource, element.syncResourceHandle); + if (machineId) { + const machines = await this.getMachines(); + const machine = machines.find(({ id }) => id === machineId); + children[0].description = machine?.isCurrent ? localize({ key: 'current', comment: ['Represents current machine'] }, "Current") : machine?.name; + } } return children; } diff --git a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts index daef57314c0..35fde76ad97 100644 --- a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts +++ b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataAutoSyncService, UserDataSyncError } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataAutoSyncService, UserDataSyncError, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; 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'; @@ -23,10 +23,11 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i constructor( @IStorageService storageService: IStorageService, @IEnvironmentService environmentService: IEnvironmentService, + @IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IInstantiationService instantiationService: IInstantiationService, @ISharedProcessService sharedProcessService: ISharedProcessService, ) { - super(storageService, environmentService); + super(storageService, environmentService, userDataSyncStoreManagementService); this.channel = sharedProcessService.getChannel('userDataAutoSync'); this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(source => this.triggerSync([source], true))); } diff --git a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution.ts b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution.ts index 34b64e790b6..36285792fe1 100644 --- a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution.ts +++ b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution.ts @@ -20,7 +20,7 @@ import { Action } from 'vs/base/common/actions'; import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/electron-browser/issue'; import { Disposable } from 'vs/base/common/lifecycle'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { CONTEXT_SYNC_STATE, SHOW_SYNC_LOG_COMMAND_ID } from 'vs/workbench/services/userDataSync/common/userDataSync'; +import { CONTEXT_SYNC_STATE, SHOW_SYNC_LOG_COMMAND_ID, SYNC_TITLE } from 'vs/workbench/services/userDataSync/common/userDataSync'; class UserDataSyncServicesContribution implements IWorkbenchContribution { @@ -49,11 +49,11 @@ class UserDataSyncReportIssueContribution extends Disposable implements IWorkben case UserDataSyncErrorCode.LocalTooManyRequests: case UserDataSyncErrorCode.TooManyRequests: const operationId = error.operationId ? localize('operationId', "Operation Id: {0}", error.operationId) : undefined; - const message = localize({ key: 'too many requests', comment: ['Preferences Sync is the name of the feature'] }, "Preferences sync is disabled because the current device is making too many requests. Please report an issue by providing the sync logs."); + const message = localize({ key: 'too many requests', comment: ['Settings Sync is the name of the feature'] }, "Settings sync is disabled because the current device is making too many requests. Please report an issue by providing the sync logs."); this.notificationService.notify({ severity: Severity.Error, message: operationId ? `${message} ${operationId}` : message, - source: error.operationId ? localize('preferences sync', "Preferences Sync. Operation Id: {0}", error.operationId) : undefined, + source: error.operationId ? localize('settings sync', "Settings Sync. Operation Id: {0}", error.operationId) : undefined, actions: { primary: [ new Action('Show Sync Logs', localize('show sync logs', "Show Log"), undefined, true, () => this.commandService.executeCommand(SHOW_SYNC_LOG_COMMAND_ID)), @@ -75,7 +75,7 @@ registerAction2(class OpenSyncBackupsFolder extends Action2 { super({ id: 'workbench.userData.actions.openSyncBackupsFolder', title: { value: localize('Open Backup folder', "Open Local Backups Folder"), original: 'Open Local Backups Folder' }, - category: { value: localize('sync preferences', "Preferences Sync"), original: `Preferences Sync` }, + category: { value: SYNC_TITLE, original: `Settings Sync` }, menu: { id: MenuId.CommandPalette, when: CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), @@ -85,8 +85,14 @@ registerAction2(class OpenSyncBackupsFolder extends Action2 { async run(accessor: ServicesAccessor): Promise { const syncHome = accessor.get(IEnvironmentService).userDataSyncHome; const electronService = accessor.get(IElectronService); - const folderStat = await accessor.get(IFileService).resolve(syncHome); - const item = folderStat.children && folderStat.children[0] ? folderStat.children[0].resource : syncHome; - return electronService.showItemInFolder(item.fsPath); + const fileService = accessor.get(IFileService); + const notificationService = accessor.get(INotificationService); + if (await fileService.exists(syncHome)) { + const folderStat = await fileService.resolve(syncHome); + const item = folderStat.children && folderStat.children[0] ? folderStat.children[0].resource : syncHome; + return electronService.showItemInFolder(item.fsPath); + } else { + notificationService.info(localize('no backups', "Local backups folder does not exist")); + } } }); diff --git a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSyncStoreManagementService.ts b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSyncStoreManagementService.ts new file mode 100644 index 00000000000..4a34eee9101 --- /dev/null +++ b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSyncStoreManagementService.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 { IUserDataSyncStoreManagementService, UserDataSyncStoreType, IUserDataSyncStore } from 'vs/platform/userDataSync/common/userDataSync'; +import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { AbstractUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { URI } from 'vs/base/common/uri'; + +export class UserDataSyncStoreManagementService extends AbstractUserDataSyncStoreManagementService implements IUserDataSyncStoreManagementService { + + private readonly channel: IChannel; + + constructor( + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService, + @IStorageService storageService: IStorageService, + @ISharedProcessService sharedProcessService: ISharedProcessService, + ) { + super(productService, configurationService, storageService); + this.channel = sharedProcessService.getChannel('userDataSyncStoreManagement'); + } + + async switch(type: UserDataSyncStoreType): Promise { + return this.channel.call('switch', [type]); + } + + async getPreviousUserDataSyncStore(): Promise { + const userDataSyncStore = await this.channel.call('getPreviousUserDataSyncStore'); + return this.revive(userDataSyncStore); + } + + private revive(userDataSyncStore: IUserDataSyncStore): IUserDataSyncStore { + return { + url: URI.revive(userDataSyncStore.url), + defaultUrl: URI.revive(userDataSyncStore.defaultUrl), + insidersUrl: URI.revive(userDataSyncStore.insidersUrl), + stableUrl: URI.revive(userDataSyncStore.stableUrl), + authenticationProviders: userDataSyncStore.authenticationProviders, + }; + } + +} diff --git a/src/vs/workbench/contrib/views/browser/treeView.ts b/src/vs/workbench/contrib/views/browser/treeView.ts index aaf4651670b..b13c36fadfb 100644 --- a/src/vs/workbench/contrib/views/browser/treeView.ts +++ b/src/vs/workbench/contrib/views/browser/treeView.ts @@ -7,11 +7,11 @@ import 'vs/css!./media/views'; import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IAction, ActionRunner } from 'vs/base/common/actions'; +import { IAction, ActionRunner, IActionViewItemProvider } from 'vs/base/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IMenuService, MenuId, MenuItemAction, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; -import { ContextAwareMenuEntryActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, MenuId, MenuItemAction, registerAction2, Action2, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { MenuEntryActionViewItem, createAndFillInContextMenuActions, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IContextKeyService, ContextKeyExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ITreeView, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, IViewDescriptorService, ViewContainer, ViewContainerLocation, ResolvableTreeItem } from 'vs/workbench/common/views'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -21,7 +21,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { ICommandService } from 'vs/platform/commands/common/commands'; import * as DOM from 'vs/base/browser/dom'; import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; -import { ActionBar, IActionViewItemProvider, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { URI } from 'vs/base/common/uri'; import { dirname, basename } from 'vs/base/common/resources'; import { LIGHT, FileThemeIcon, FolderThemeIcon, registerThemingParticipant, ThemeIcon, IThemeService } from 'vs/platform/theme/common/themeService'; @@ -39,6 +39,7 @@ 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 { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; class Root implements ITreeItem { label = { label: 'root' }; @@ -349,7 +350,15 @@ export class TreeView extends Disposable implements ITreeView { } private createTree() { - const actionViewItemProvider = (action: IAction) => action instanceof MenuItemAction ? this.instantiationService.createInstance(ContextAwareMenuEntryActionViewItem, action) : undefined; + const actionViewItemProvider = (action: IAction) => { + if (action instanceof MenuItemAction) { + return this.instantiationService.createInstance(MenuEntryActionViewItem, action); + } else if (action instanceof SubmenuItemAction) { + return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); + } + + return undefined; + }; const treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id)); this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); const dataSource = this.instantiationService.createInstance(TreeDataSource, this, (task: Promise) => this.progressService.withProgress({ location: this.id }, () => task)); @@ -980,7 +989,6 @@ class TreeMenus extends Disposable implements IDisposable { createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => /^inline/.test(g)); menu.dispose(); - contextKeyService.dispose(); return result; } diff --git a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts index 3ca6ae7287b..0291f0c605d 100644 --- a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts @@ -8,14 +8,16 @@ import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ILogService } from 'vs/platform/log/common/log'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; import { WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; import { areWebviewInputOptionsEqual } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; -import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { ILogService } from 'vs/platform/log/common/log'; export const enum WebviewMessageChannels { onmessage = 'onmessage', @@ -29,7 +31,8 @@ export const enum WebviewMessageChannels { loadResource = 'load-resource', loadLocalhost = 'load-localhost', webviewReady = 'webview-ready', - wheel = 'did-scroll-wheel' + wheel = 'did-scroll-wheel', + fatalError = 'fatal-error', } interface IKeydownEvent { @@ -84,6 +87,7 @@ export abstract class BaseWebview extends Disposable { contentOptions: WebviewContentOptions, public readonly extension: WebviewExtensionDescription | undefined, private readonly webviewThemeDataProvider: WebviewThemeDataProvider, + @INotificationService notificationService: INotificationService, @ILogService private readonly _logService: ILogService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IEnvironmentService private readonly _environmentService: IEnvironmentService, @@ -151,6 +155,10 @@ export abstract class BaseWebview extends Disposable { this.handleFocusChange(false); })); + this._register(this.on<{ message: string }>(WebviewMessageChannels.fatalError, (e) => { + notificationService.error(localize('fatalErrorMessage', "Error loading webview: {0}", e.message)); + })); + this._register(this.on('did-keydown', (data: KeyboardEvent) => { // Electron: workaround for https://github.com/electron/electron/issues/14258 // We have to detect keyboard events in the and dispatch them to our diff --git a/src/vs/workbench/contrib/webview/browser/pre/host.js b/src/vs/workbench/contrib/webview/browser/pre/host.js index eafc07ad8cd..c63f6122aee 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/host.js +++ b/src/vs/workbench/contrib/webview/browser/pre/host.js @@ -36,39 +36,49 @@ } }(); + function fatalError(/** @type {string} */ message) { + console.error(`Webview fatal error: ${message}`); + hostMessaging.postMessage('fatal-error', { message }); + } + const workerReady = new Promise(async (resolveWorkerReady) => { if (onElectron) { return resolveWorkerReady(); } if (!areServiceWorkersEnabled()) { - console.log('Service Workers are not enabled. Webviews will not work properly'); + fatalError('Service Workers are not enabled in browser. Webviews will not work.'); return resolveWorkerReady(); } const expectedWorkerVersion = 1; - navigator.serviceWorker.register('service-worker.js').then(async registration => { - await navigator.serviceWorker.ready; + navigator.serviceWorker.register('service-worker.js').then( + async registration => { + await navigator.serviceWorker.ready; - const versionHandler = (event) => { - if (event.data.channel !== 'version') { - return; - } + const versionHandler = (event) => { + if (event.data.channel !== 'version') { + return; + } - navigator.serviceWorker.removeEventListener('message', versionHandler); - if (event.data.version === expectedWorkerVersion) { - return resolveWorkerReady(); - } else { - // If we have the wrong version, try once to unregister and re-register - return registration.update() - .then(() => navigator.serviceWorker.ready) - .finally(resolveWorkerReady); - } - }; - navigator.serviceWorker.addEventListener('message', versionHandler); - registration.active.postMessage({ channel: 'version' }); - }); + navigator.serviceWorker.removeEventListener('message', versionHandler); + if (event.data.version === expectedWorkerVersion) { + return resolveWorkerReady(); + } else { + // If we have the wrong version, try once to unregister and re-register + return registration.update() + .then(() => navigator.serviceWorker.ready) + .finally(resolveWorkerReady); + } + }; + navigator.serviceWorker.addEventListener('message', versionHandler); + registration.active.postMessage({ channel: 'version' }); + }, + error => { + fatalError(`Could not register service workers: ${error}.`); + resolveWorkerReady(); + }); const forwardFromHostToWorker = (channel) => { hostMessaging.onMessage(channel, event => { diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index 6d525c43935..630d5d93422 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -496,21 +496,45 @@ newFrame.contentDocument.open(); } - newFrame.contentWindow.addEventListener('DOMContentLoaded', e => { + /** + * @param {Document} contentDocument + */ + function onFrameLoaded(contentDocument) { // Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=978325 setTimeout(() => { if (host.fakeLoad) { - newFrame.contentDocument.open(); - newFrame.contentDocument.write(newDocument); - newFrame.contentDocument.close(); + contentDocument.open(); + contentDocument.write(newDocument); + contentDocument.close(); hookupOnLoadHandlers(newFrame); } - const contentDocument = e.target ? (/** @type {HTMLDocument} */ (e.target)) : undefined; if (contentDocument) { applyStyles(contentDocument, contentDocument.body); } }, 0); - }); + } + + if (host.fakeLoad) { + // On Safari for iframes with scripts disabled, the `DOMContentLoaded` never seems to be fired. + // Use polling instead. + const interval = setInterval(() => { + // If the frame is no longer mounted, loading has stopped + if (!newFrame.parentElement) { + clearInterval(interval); + return; + } + + if (newFrame.contentDocument.readyState === 'complete') { + clearInterval(interval); + onFrameLoaded(newFrame.contentDocument); + } + }, 10); + } else { + newFrame.contentWindow.addEventListener('DOMContentLoaded', e => { + const contentDocument = e.target ? (/** @type {HTMLDocument} */ (e.target)) : undefined; + onFrameLoaded(contentDocument); + }); + } /** * @param {Document} contentDocument diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index d47184b8afa..792d98a57bb 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -11,6 +11,7 @@ import { URI } from 'vs/base/common/uri'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; @@ -32,6 +33,7 @@ export class IFrameWebview extends BaseWebview implements Web contentOptions: WebviewContentOptions, extension: WebviewExtensionDescription | undefined, webviewThemeDataProvider: WebviewThemeDataProvider, + @INotificationService notificationService: INotificationService, @ITunnelService tunnelService: ITunnelService, @IFileService private readonly fileService: IFileService, @IRequestService private readonly requestService: IRequestService, @@ -41,7 +43,7 @@ export class IFrameWebview extends BaseWebview implements Web @IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, @ILogService logService: ILogService, ) { - super(id, options, contentOptions, extension, webviewThemeDataProvider, logService, telemetryService, environmentService, _workbenchEnvironmentService); + super(id, options, contentOptions, extension, webviewThemeDataProvider, notificationService, logService, telemetryService, environmentService, _workbenchEnvironmentService); this._portMappingManager = this._register(new WebviewPortMappingManager( () => this.extension?.location, diff --git a/src/vs/workbench/contrib/webview/electron-browser/iframeWebviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/iframeWebviewElement.ts index facaa43b65a..6e7570d370a 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/iframeWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/iframeWebviewElement.ts @@ -10,6 +10,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { IRequestService } from 'vs/platform/request/common/request'; @@ -46,9 +47,10 @@ export class ElectronIframeWebview extends IFrameWebview { @IRemoteAuthorityResolverService _remoteAuthorityResolverService: IRemoteAuthorityResolverService, @ILogService logService: ILogService, @IInstantiationService instantiationService: IInstantiationService, + @INotificationService noficationService: INotificationService, ) { super(id, options, contentOptions, extension, webviewThemeDataProvider, - tunnelService, fileService, requestService, telemetryService, environmentService, _workbenchEnvironmentService, _remoteAuthorityResolverService, logService); + noficationService, tunnelService, fileService, requestService, telemetryService, environmentService, _workbenchEnvironmentService, _remoteAuthorityResolverService, logService); this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options, Promise.resolve(undefined))); } diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 0ad394f8d3d..7421ee686ff 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -18,6 +18,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { ILogService } from 'vs/platform/log/common/log'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { webviewPartitionId } from 'vs/platform/webview/common/resourceLoader'; import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService'; @@ -26,7 +27,7 @@ import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/t import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { WebviewFindDelegate, WebviewFindWidget } from '../browser/webviewFindWidget'; -import { WebviewResourceRequestManager, rewriteVsCodeResourceUrls } from './resourceLoading'; +import { rewriteVsCodeResourceUrls, WebviewResourceRequestManager } from './resourceLoading'; class WebviewKeyboardHandler { @@ -124,8 +125,9 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme @IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService, @IConfigurationService configurationService: IConfigurationService, @IMainProcessService mainProcessService: IMainProcessService, + @INotificationService noficationService: INotificationService, ) { - super(id, options, contentOptions, extension, _webviewThemeDataProvider, _myLogService, telemetryService, environmentService, workbenchEnvironmentService); + super(id, options, contentOptions, extension, _webviewThemeDataProvider, noficationService, _myLogService, telemetryService, environmentService, workbenchEnvironmentService); /* __GDPR__ "webview.createWebview" : { diff --git a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts index abe57616814..6378a16dddf 100644 --- a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts +++ b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts @@ -162,7 +162,7 @@ interface ExtensionSuggestion { const extensionPacks: ExtensionSuggestion[] = [ { name: localize('welcomePage.javaScript', "JavaScript"), id: 'dbaeumer.vscode-eslint' }, { name: localize('welcomePage.python', "Python"), id: 'ms-python.python' }, - // { name: localize('welcomePage.go', "Go"), id: 'lukehoban.go' }, + { name: localize('welcomePage.java', "Java"), id: 'vscjava.vscode-java-pack' }, { name: localize('welcomePage.php', "PHP"), id: 'felixfbecker.php-pack' }, { name: localize('welcomePage.azure', "Azure"), title: localize('welcomePage.showAzureExtensions', "Show Azure extensions"), id: 'workbench.extensions.action.showAzureExtensions', isCommand: true }, { name: localize('welcomePage.docker', "Docker"), id: 'ms-azuretools.vscode-docker' }, diff --git a/src/vs/workbench/electron-browser/desktop.main.ts b/src/vs/workbench/electron-browser/desktop.main.ts index 76f5e7bfb07..06e8e0137c1 100644 --- a/src/vs/workbench/electron-browser/desktop.main.ts +++ b/src/vs/workbench/electron-browser/desktop.main.ts @@ -195,7 +195,7 @@ class DesktopMain extends Disposable { serviceCollection.set(ISignService, signService); // Remote Agent - const remoteAgentService = this._register(new RemoteAgentService(this.environmentService, remoteAuthorityResolverService, signService, logService, productService)); + const remoteAgentService = this._register(new RemoteAgentService(this.environmentService, productService, remoteAuthorityResolverService, signService, logService)); serviceCollection.set(IRemoteAgentService, remoteAgentService); // Electron diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 6b06f122a5e..793b2394597 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -8,8 +8,7 @@ import { URI } from 'vs/base/common/uri'; import * as errors from 'vs/base/common/errors'; import { equals, deepClone } from 'vs/base/common/objects'; import * as DOM from 'vs/base/browser/dom'; -import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IAction } from 'vs/base/common/actions'; +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'; diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 202a92003fa..9741fd8fbcf 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { AuthenticationSession, AuthenticationSessionsChangeEvent, AuthenticationProviderInformation } from 'vs/editor/common/modes'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -15,6 +15,9 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; 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'; + +export function getAuthenticationProviderActivationEvent(id: string): string { return `onAuthenticationRequest:${id}`; } export const IAuthenticationService = createDecorator('IAuthenticationService'); @@ -68,12 +71,17 @@ export interface SessionRequestInfo { [scopes: string]: SessionRequest; } +CommandsRegistry.registerCommand('workbench.getCodeExchangeProxyEndpoints', function (accessor, _) { + const environmentService = accessor.get(IWorkbenchEnvironmentService); + return environmentService.options?.codeExchangeProxyEndpoints; +}); + export class AuthenticationService extends Disposable implements IAuthenticationService { declare readonly _serviceBrand: undefined; private _placeholderMenuItem: IDisposable | undefined; private _noAccountsMenuItem: IDisposable | undefined; private _signInRequestItems = new Map(); - private _badgeDisposable: IDisposable | undefined; + private _accountBadgeDisposable = this._register(new MutableDisposable()); private _authenticationProviders: Map = new Map(); @@ -203,10 +211,9 @@ export class AuthenticationService extends Disposable implements IAuthentication }); if (changed) { - if (this._signInRequestItems.size === 0) { - this._badgeDisposable?.dispose(); - this._badgeDisposable = undefined; - } else { + this._accountBadgeDisposable.clear(); + + if (this._signInRequestItems.size > 0) { let numberOfRequests = 0; this._signInRequestItems.forEach(providerRequests => { Object.keys(providerRequests).forEach(request => { @@ -215,13 +222,27 @@ export class AuthenticationService extends Disposable implements IAuthentication }); const badge = new NumberBadge(numberOfRequests, () => nls.localize('sign in', "Sign in requested")); - this._badgeDisposable = this.activityService.showAccountsActivity({ badge }); + this._accountBadgeDisposable.value = this.activityService.showAccountsActivity({ badge }); } } } - requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): void { - const provider = this._authenticationProviders.get(providerId); + async requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise { + let provider = this._authenticationProviders.get(providerId); + if (!provider) { + // Activate has already been called for the authentication provider, but it cannot block on registering itself + // since this is sync and returns a disposable. So, wait for registration event to fire that indicates the + // provider is now in the map. + await new Promise((resolve, _) => { + this.onDidRegisterAuthenticationProvider(e => { + if (e.id === providerId) { + provider = this._authenticationProviders.get(providerId); + resolve(); + } + }); + }); + } + if (provider) { const providerRequests = this._signInRequestItems.get(providerId); const scopesList = scopes.sort().join(''); @@ -284,6 +305,8 @@ export class AuthenticationService extends Disposable implements IAuthentication }); } + this._accountBadgeDisposable.clear(); + let numberOfRequests = 0; this._signInRequestItems.forEach(providerRequests => { Object.keys(providerRequests).forEach(request => { @@ -292,7 +315,7 @@ export class AuthenticationService extends Disposable implements IAuthentication }); const badge = new NumberBadge(numberOfRequests, () => nls.localize('sign in', "Sign in requested")); - this._badgeDisposable = this.activityService.showAccountsActivity({ badge }); + this._accountBadgeDisposable.value = this.activityService.showAccountsActivity({ badge }); } } getLabel(id: string): string { diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index d25346d4ac2..747f5506106 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -94,8 +94,12 @@ export class WorkspaceService extends Disposable implements IConfigurationServic }); })); - this._register(Registry.as(Extensions.Configuration).onDidSchemaChange(e => this.registerConfigurationSchemas())); - this._register(Registry.as(Extensions.Configuration).onDidUpdateConfiguration(configurationProperties => this.onDefaultConfigurationChanged(configurationProperties))); + 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(); } @@ -506,6 +510,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic const folderConfiguration = this.cachedFolderConfigs.get(this.workspace.folders[0].uri); if (folderConfiguration) { this._configuration.updateWorkspaceConfiguration(folderConfiguration.reprocess()); + this._configuration.updateFolderConfiguration(this.workspace.folders[0].uri, folderConfiguration.reprocess()); } } else { this._configuration.updateWorkspaceConfiguration(this.workspaceConfiguration.reprocessWorkspaceSettings()); @@ -523,12 +528,13 @@ 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, allSettingsSchema); + jsonRegistry.registerSchema(defaultSettingsSchemaId, defaultSettingsSchema); jsonRegistry.registerSchema(userSettingsSchemaId, userSettingsSchema); jsonRegistry.registerSchema(machineSettingsSchemaId, machineSettingsSchema); diff --git a/src/vs/workbench/services/configuration/common/configurationModels.ts b/src/vs/workbench/services/configuration/common/configurationModels.ts index 5159bd43fe8..ad60a05ab72 100644 --- a/src/vs/workbench/services/configuration/common/configurationModels.ts +++ b/src/vs/workbench/services/configuration/common/configurationModels.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { equals } from 'vs/base/common/objects'; -import { toValuesTree, IConfigurationModel, IConfigurationOverrides, IConfigurationValue, IConfigurationChange, overrideIdentifierFromKey } from 'vs/platform/configuration/common/configuration'; +import { toValuesTree, IConfigurationModel, IConfigurationOverrides, IConfigurationValue, IConfigurationChange } from 'vs/platform/configuration/common/configuration'; import { Configuration as BaseConfiguration, ConfigurationModelParser, ConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; import { IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import { Workspace } from 'vs/platform/workspace/common/workspace'; import { ResourceMap } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; import { WORKSPACE_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; -import { OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry'; +import { OVERRIDE_PROPERTY_PATTERN, overrideIdentifierFromKey } from 'vs/platform/configuration/common/configurationRegistry'; export class WorkspaceConfigurationModelParser extends ConfigurationModelParser { 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 13933070345..5091446162d 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 @@ -112,7 +112,7 @@ suite('WorkspaceContextService - Folder', () => { const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService()); fileService.registerProvider(Schemas.file, diskFileSystemProvider); fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, new DiskFileSystemProvider(new NullLogService()), environmentService, new NullLogService())); - workspaceContextService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, new RemoteAgentService(environmentService, new RemoteAuthorityResolverService(), new SignService(undefined), new NullLogService(), { _serviceBrand: undefined, ...product })); + workspaceContextService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, new RemoteAgentService(environmentService, { _serviceBrand: undefined, ...product }, new RemoteAuthorityResolverService(), new SignService(undefined), new NullLogService())); return (workspaceContextService).initialize(convertToWorkspacePayload(URI.file(folderDir))); }); }); @@ -882,56 +882,140 @@ suite('WorkspaceConfigurationService - Folder', () => { }); }); - test('application settings are not read from workspace', () => { + test('application settings are not read from workspace', async () => { fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.applicationSetting": "userValue" }'); fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.applicationSetting": "workspaceValue" }'); - return testObject.reloadConfiguration() - .then(() => assert.equal(testObject.getValue('configurationService.folder.applicationSetting'), 'userValue')); + + await testObject.reloadConfiguration(); + + assert.equal(testObject.getValue('configurationService.folder.applicationSetting'), 'userValue'); }); - test('machine settings are not read from workspace', () => { + test('application settings are not read from workspace when workspace folder uri is passed', async () => { + fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.applicationSetting": "userValue" }'); + fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.applicationSetting": "workspaceValue" }'); + + await testObject.reloadConfiguration(); + + assert.equal(testObject.getValue('configurationService.folder.applicationSetting', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); + }); + + test('machine settings are not read from workspace', async () => { fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.machineSetting": "userValue" }'); fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.machineSetting": "workspaceValue" }'); - return testObject.reloadConfiguration() - .then(() => assert.equal(testObject.getValue('configurationService.folder.machineSetting'), 'userValue')); + + await testObject.reloadConfiguration(); + + assert.equal(testObject.getValue('configurationService.folder.machineSetting', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); }); - test('get application scope settings are not loaded after defaults are registered', () => { - fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.anotherApplicationSetting": "workspaceValue" }'); - return testObject.reloadConfiguration() - .then(() => { - configurationRegistry.registerConfiguration({ - 'id': '_test', - 'type': 'object', - 'properties': { - 'configurationService.folder.anotherApplicationSetting': { - 'type': 'string', - 'default': 'isSet', - scope: ConfigurationScope.APPLICATION - } - } - }); - assert.deepEqual(testObject.keys().workspace, []); - }); + test('machine settings are not read from workspace when workspace folder uri is passed', async () => { + fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.machineSetting": "userValue" }'); + fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.machineSetting": "workspaceValue" }'); + + await testObject.reloadConfiguration(); + + assert.equal(testObject.getValue('configurationService.folder.machineSetting', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); }); - test('get machine scope settings are not loaded after defaults are registered', () => { - fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.anotherMachineSetting": "workspaceValue" }'); - return testObject.reloadConfiguration() - .then(() => { - configurationRegistry.registerConfiguration({ - 'id': '_test', - 'type': 'object', - 'properties': { - 'configurationService.folder.anotherMachineSetting': { - 'type': 'string', - 'default': 'isSet', - scope: ConfigurationScope.MACHINE - } - } - }); - assert.deepEqual(testObject.keys().workspace, []); - }); + test('get application scope settings are not loaded after defaults are registered', async () => { + fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.applicationSetting-2": "userValue" }'); + fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.applicationSetting-2": "workspaceValue" }'); + + await testObject.reloadConfiguration(); + assert.equal(testObject.getValue('configurationService.folder.applicationSetting-2'), 'workspaceValue'); + + configurationRegistry.registerConfiguration({ + 'id': '_test', + 'type': 'object', + 'properties': { + 'configurationService.folder.applicationSetting-2': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.APPLICATION + } + } + }); + + assert.equal(testObject.getValue('configurationService.folder.applicationSetting-2'), 'userValue'); + + await testObject.reloadConfiguration(); + assert.equal(testObject.getValue('configurationService.folder.applicationSetting-2'), 'userValue'); + }); + + test('get application scope settings are not loaded after defaults are registered when workspace folder uri is passed', async () => { + fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.applicationSetting-3": "userValue" }'); + fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.applicationSetting-3": "workspaceValue" }'); + + await testObject.reloadConfiguration(); + assert.equal(testObject.getValue('configurationService.folder.applicationSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'workspaceValue'); + + configurationRegistry.registerConfiguration({ + 'id': '_test', + 'type': 'object', + 'properties': { + 'configurationService.folder.applicationSetting-3': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.APPLICATION + } + } + }); + + assert.equal(testObject.getValue('configurationService.folder.applicationSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); + + await testObject.reloadConfiguration(); + assert.equal(testObject.getValue('configurationService.folder.applicationSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); + }); + + test('get machine scope settings are not loaded after defaults are registered', async () => { + fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.machineSetting-2": "userValue" }'); + fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.machineSetting-2": "workspaceValue" }'); + + await testObject.reloadConfiguration(); + assert.equal(testObject.getValue('configurationService.folder.machineSetting-2'), 'workspaceValue'); + + configurationRegistry.registerConfiguration({ + 'id': '_test', + 'type': 'object', + 'properties': { + 'configurationService.folder.machineSetting-2': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.MACHINE + } + } + }); + + assert.equal(testObject.getValue('configurationService.folder.machineSetting-2'), 'userValue'); + + await testObject.reloadConfiguration(); + assert.equal(testObject.getValue('configurationService.folder.machineSetting-2'), 'userValue'); + }); + + test('get machine scope settings are not loaded after defaults are registered when workspace folder uri is passed', async () => { + fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.machineSetting-3": "userValue" }'); + fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.machineSetting-3": "workspaceValue" }'); + + await testObject.reloadConfiguration(); + assert.equal(testObject.getValue('configurationService.folder.machineSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'workspaceValue'); + + configurationRegistry.registerConfiguration({ + 'id': '_test', + 'type': 'object', + 'properties': { + 'configurationService.folder.machineSetting-3': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.MACHINE + } + } + }); + + assert.equal(testObject.getValue('configurationService.folder.machineSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); + + await testObject.reloadConfiguration(); + assert.equal(testObject.getValue('configurationService.folder.machineSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); }); test('reload configuration emits events after global configuraiton changes', () => { @@ -1231,111 +1315,202 @@ suite('WorkspaceConfigurationService-Multiroot', () => { return undefined; }); - test('application settings are not read from workspace', () => { - fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.applicationSetting": "userValue" }'); - return jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.applicationSetting': 'workspaceValue' } }], true) - .then(() => testObject.reloadConfiguration()) - .then(() => assert.equal(testObject.getValue('configurationService.workspace.applicationSetting'), 'userValue')); + test('application settings are not read from workspace', async () => { + fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.applicationSetting": "userValue" }'); + await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.applicationSetting': 'workspaceValue' } }], true); + + await testObject.reloadConfiguration(); + + assert.equal(testObject.getValue('configurationService.folder.applicationSetting'), 'userValue'); }); - test('machine settings are not read from workspace', () => { - fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.machineSetting": "userValue" }'); - return jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.machineSetting': 'workspaceValue' } }], true) - .then(() => testObject.reloadConfiguration()) - .then(() => assert.equal(testObject.getValue('configurationService.workspace.machineSetting'), 'userValue')); + test('application settings are not read from workspace when folder is passed', async () => { + fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.applicationSetting": "userValue" }'); + await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.applicationSetting': 'workspaceValue' } }], true); + + await testObject.reloadConfiguration(); + + assert.equal(testObject.getValue('configurationService.folder.applicationSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue'); }); - test('workspace settings override user settings after defaults are registered ', () => { + test('machine settings are not read from workspace', async () => { + fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.machineSetting": "userValue" }'); + await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.machineSetting': 'workspaceValue' } }], true); + + await testObject.reloadConfiguration(); + + assert.equal(testObject.getValue('configurationService.folder.machineSetting'), 'userValue'); + }); + + test('machine settings are not read from workspace when folder is passed', async () => { + fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.machineSetting": "userValue" }'); + await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.machineSetting': 'workspaceValue' } }], true); + + await testObject.reloadConfiguration(); + + assert.equal(testObject.getValue('configurationService.folder.machineSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue'); + }); + + test('get application scope settings are not loaded after defaults are registered', async () => { fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.newSetting": "userValue" }'); - return jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.newSetting': 'workspaceValue' } }], true) - .then(() => testObject.reloadConfiguration()) - .then(() => { - configurationRegistry.registerConfiguration({ - 'id': '_test', - 'type': 'object', - 'properties': { - 'configurationService.workspace.newSetting': { - 'type': 'string', - 'default': 'isSet' - } - } - }); - assert.equal(testObject.getValue('configurationService.workspace.newSetting'), 'workspaceValue'); - }); + await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.newSetting': 'workspaceValue' } }], true); + + await testObject.reloadConfiguration(); + assert.equal(testObject.getValue('configurationService.workspace.newSetting'), 'workspaceValue'); + + configurationRegistry.registerConfiguration({ + 'id': '_test', + 'type': 'object', + 'properties': { + 'configurationService.workspace.newSetting': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.APPLICATION + } + } + }); + + assert.equal(testObject.getValue('configurationService.workspace.newSetting'), 'userValue'); + + await testObject.reloadConfiguration(); + assert.equal(testObject.getValue('configurationService.workspace.newSetting'), 'userValue'); }); - test('workspace settings override user settings after defaults are registered for machine overridable settings ', () => { + test('get application scope settings are not loaded after defaults are registered when workspace folder is passed', async () => { + fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.newSetting-2": "userValue" }'); + await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.newSetting-2': 'workspaceValue' } }], true); + + await testObject.reloadConfiguration(); + assert.equal(testObject.getValue('configurationService.workspace.newSetting-2', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'workspaceValue'); + + configurationRegistry.registerConfiguration({ + 'id': '_test', + 'type': 'object', + 'properties': { + 'configurationService.workspace.newSetting-2': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.APPLICATION + } + } + }); + + assert.equal(testObject.getValue('configurationService.workspace.newSetting-2', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue'); + + await testObject.reloadConfiguration(); + assert.equal(testObject.getValue('configurationService.workspace.newSetting-2', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue'); + }); + + test('workspace settings override user settings after defaults are registered for machine overridable settings ', async () => { fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.newMachineOverridableSetting": "userValue" }'); - return jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.newMachineOverridableSetting': 'workspaceValue' } }], true) - .then(() => testObject.reloadConfiguration()) - .then(() => { - configurationRegistry.registerConfiguration({ - 'id': '_test', - 'type': 'object', - 'properties': { - 'configurationService.workspace.newMachineOverridableSetting': { - 'type': 'string', - 'default': 'isSet', - scope: ConfigurationScope.MACHINE_OVERRIDABLE - } - } - }); - assert.equal(testObject.getValue('configurationService.workspace.newMachineOverridableSetting'), 'workspaceValue'); - }); + await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.newMachineOverridableSetting': 'workspaceValue' } }], true); + + await testObject.reloadConfiguration(); + assert.equal(testObject.getValue('configurationService.workspace.newMachineOverridableSetting'), 'workspaceValue'); + + configurationRegistry.registerConfiguration({ + 'id': '_test', + 'type': 'object', + 'properties': { + 'configurationService.workspace.newMachineOverridableSetting': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.MACHINE_OVERRIDABLE + } + } + }); + + assert.equal(testObject.getValue('configurationService.workspace.newMachineOverridableSetting'), 'workspaceValue'); + + await testObject.reloadConfiguration(); + assert.equal(testObject.getValue('configurationService.workspace.newMachineOverridableSetting'), 'workspaceValue'); + }); - test('application settings are not read from workspace folder', () => { + test('application settings are not read from workspace folder', async () => { fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.applicationSetting": "userValue" }'); fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.applicationSetting": "workspaceFolderValue" }'); - return testObject.reloadConfiguration() - .then(() => assert.equal(testObject.getValue('configurationService.workspace.applicationSetting'), 'userValue')); + + await testObject.reloadConfiguration(); + + assert.equal(testObject.getValue('configurationService.workspace.applicationSetting'), 'userValue'); }); - test('machine settings are not read from workspace folder', () => { + test('application settings are not read from workspace folder when workspace folder is passed', async () => { + fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.applicationSetting": "userValue" }'); + fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.applicationSetting": "workspaceFolderValue" }'); + + await testObject.reloadConfiguration(); + + assert.equal(testObject.getValue('configurationService.workspace.applicationSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue'); + }); + + test('machine settings are not read from workspace folder', async () => { fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.machineSetting": "userValue" }'); fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.machineSetting": "workspaceFolderValue" }'); - return testObject.reloadConfiguration() - .then(() => assert.equal(testObject.getValue('configurationService.workspace.machineSetting'), 'userValue')); + + await testObject.reloadConfiguration(); + + assert.equal(testObject.getValue('configurationService.workspace.machineSetting'), 'userValue'); }); - test('application settings are not read from workspace folder after defaults are registered', () => { + test('machine settings are not read from workspace folder when workspace folder is passed', async () => { + fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.machineSetting": "userValue" }'); + fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.machineSetting": "workspaceFolderValue" }'); + + await testObject.reloadConfiguration(); + + assert.equal(testObject.getValue('configurationService.workspace.machineSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue'); + }); + + test('application settings are not read from workspace folder after defaults are registered', async () => { fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.testNewApplicationSetting": "userValue" }'); fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.testNewApplicationSetting": "workspaceFolderValue" }'); - return testObject.reloadConfiguration() - .then(() => { - configurationRegistry.registerConfiguration({ - 'id': '_test', - 'type': 'object', - 'properties': { - 'configurationService.workspace.testNewApplicationSetting': { - 'type': 'string', - 'default': 'isSet', - scope: ConfigurationScope.APPLICATION - } - } - }); - assert.equal(testObject.getValue('configurationService.workspace.testNewApplicationSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue'); - }); + + await testObject.reloadConfiguration(); + assert.equal(testObject.getValue('configurationService.workspace.testNewApplicationSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'workspaceFolderValue'); + + configurationRegistry.registerConfiguration({ + 'id': '_test', + 'type': 'object', + 'properties': { + 'configurationService.workspace.testNewApplicationSetting': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.APPLICATION + } + } + }); + + assert.equal(testObject.getValue('configurationService.workspace.testNewApplicationSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue'); + + await testObject.reloadConfiguration(); + assert.equal(testObject.getValue('configurationService.workspace.testNewApplicationSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue'); }); - test('application settings are not read from workspace folder after defaults are registered', () => { + test('application settings are not read from workspace folder after defaults are registered', async () => { fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.testNewMachineSetting": "userValue" }'); fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.testNewMachineSetting": "workspaceFolderValue" }'); - return testObject.reloadConfiguration() - .then(() => { - configurationRegistry.registerConfiguration({ - 'id': '_test', - 'type': 'object', - 'properties': { - 'configurationService.workspace.testNewMachineSetting': { - 'type': 'string', - 'default': 'isSet', - scope: ConfigurationScope.MACHINE - } - } - }); - assert.equal(testObject.getValue('configurationService.workspace.testNewMachineSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue'); - }); + await testObject.reloadConfiguration(); + + assert.equal(testObject.getValue('configurationService.workspace.testNewMachineSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'workspaceFolderValue'); + + configurationRegistry.registerConfiguration({ + 'id': '_test', + 'type': 'object', + 'properties': { + 'configurationService.workspace.testNewMachineSetting': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.MACHINE + } + } + }); + + assert.equal(testObject.getValue('configurationService.workspace.testNewMachineSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue'); + + await testObject.reloadConfiguration(); + assert.equal(testObject.getValue('configurationService.workspace.testNewMachineSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue'); }); test('resource setting in folder is read after it is registered later', () => { diff --git a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts index d041ca9054d..ee78a258ee3 100644 --- a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts +++ b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IAction, IActionRunner, ActionRunner, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; -import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IAction, IActionRunner, ActionRunner, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, Separator, SubmenuAction } from 'vs/base/common/actions'; import * as dom from 'vs/base/browser/dom'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -13,7 +12,7 @@ import { getZoomFactor } from 'vs/base/browser/browser'; import { unmnemonicLabel } from 'vs/base/common/labels'; import { Event, Emitter } from 'vs/base/common/event'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IContextMenuDelegate, ContextSubMenu, IContextMenuEvent } from 'vs/base/browser/contextmenu'; +import { IContextMenuDelegate, IContextMenuEvent } from 'vs/base/browser/contextmenu'; import { once } from 'vs/base/common/functional'; import { Disposable } from 'vs/base/common/lifecycle'; import { IContextMenuItem } from 'vs/base/parts/contextmenu/common/contextmenu'; @@ -26,6 +25,7 @@ import { ContextMenuService as HTMLContextMenuService } from 'vs/platform/contex import { IThemeService } from 'vs/platform/theme/common/themeService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { stripCodicons } from 'vs/base/common/codicons'; +import { coalesce } from 'vs/base/common/arrays'; export class ContextMenuService extends Disposable implements IContextMenuService { @@ -124,24 +124,28 @@ class NativeContextMenuService extends Disposable implements IContextMenuService } } - private createMenu(delegate: IContextMenuDelegate, entries: ReadonlyArray, onHide: () => void): IContextMenuItem[] { + private createMenu(delegate: IContextMenuDelegate, entries: IAction[], onHide: () => void, submenuIds = new Set()): IContextMenuItem[] { const actionRunner = delegate.actionRunner || new ActionRunner(); - - return entries.map(entry => this.createMenuItem(delegate, entry, actionRunner, onHide)); + return coalesce(entries.map(entry => this.createMenuItem(delegate, entry, actionRunner, onHide, submenuIds))); } - private createMenuItem(delegate: IContextMenuDelegate, entry: IAction | ContextSubMenu, actionRunner: IActionRunner, onHide: () => void): IContextMenuItem { - + private createMenuItem(delegate: IContextMenuDelegate, entry: IAction, actionRunner: IActionRunner, onHide: () => void, submenuIds: Set): IContextMenuItem | undefined { // Separator if (entry instanceof Separator) { return { type: 'separator' }; } // Submenu - if (entry instanceof ContextSubMenu) { + if (entry instanceof SubmenuAction) { + if (submenuIds.has(entry.id)) { + console.warn(`Found submenu cycle: ${entry.id}`); + return undefined; + } + + const actions = Array.isArray(entry.actions) ? entry.actions : entry.actions(); return { label: unmnemonicLabel(stripCodicons(entry.label)).trim(), - submenu: this.createMenu(delegate, entry.entries, onHide) + submenu: this.createMenu(delegate, actions, onHide, new Set([...submenuIds, entry.id])) }; } diff --git a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts index 550b9001bb2..64f90d318ad 100644 --- a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts @@ -25,7 +25,6 @@ import { coalesce } from 'vs/base/common/arrays'; import { trim } from 'vs/base/common/strings'; import { IModeService } from 'vs/editor/common/services/modeService'; import { ILabelService } from 'vs/platform/label/common/label'; -import { isWindows } from 'vs/base/common/platform'; export abstract class AbstractFileDialogService implements IFileDialogService { @@ -259,7 +258,7 @@ export abstract class AbstractFileDialogService implements IFileDialogService { // Build the file filter by using our known languages const ext: string | undefined = defaultUri ? resources.extname(defaultUri) : undefined; let matchingFilter: IFilter | undefined; - const filters: IFilter[] = coalesce(this.modeService.getRegisteredLanguageNames().map(languageName => { + const registeredLanguageFilters: IFilter[] = coalesce(this.modeService.getRegisteredLanguageNames().map(languageName => { const extensions = this.modeService.getExtensions(languageName); if (!extensions || !extensions.length) { return null; @@ -279,24 +278,20 @@ export abstract class AbstractFileDialogService implements IFileDialogService { // We have no matching filter, e.g. because the language // is unknown. We still add the extension to the list of // filters though so that it can be picked - // (https://github.com/microsoft/vscode/issues/96283) but - // only on Windows where this is an issue. Adding this to - // macOS would result in the following bugs: - // https://github.com/microsoft/vscode/issues/100614 and - // https://github.com/microsoft/vscode/issues/100241 - if (isWindows && !matchingFilter && ext) { + // (https://github.com/microsoft/vscode/issues/96283) + if (!matchingFilter && ext) { matchingFilter = { name: trim(ext, '.').toUpperCase(), extensions: [trim(ext, '.')] }; } // Order of filters is - // - File Extension Match - // - All Files + // - All Files (we MUST do this to fix macOS issue https://github.com/microsoft/vscode/issues/102713) + // - File Extension Match (if any) // - All Languages // - No Extension options.filters = coalesce([ - matchingFilter, { name: nls.localize('allFiles', "All Files"), extensions: ['*'] }, - ...filters, + matchingFilter, + ...registeredLanguageFilters, { name: nls.localize('noExt', "No Extension"), extensions: [''] } ]); diff --git a/src/vs/workbench/services/dialogs/browser/dialogService.ts b/src/vs/workbench/services/dialogs/browser/dialogService.ts index bf9a892d41e..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']; + private allowableCommands = ['copy', 'cut', 'editor.action.clipboardCopyAction', 'editor.action.clipboardCutAction']; constructor( @ILogService private readonly logService: ILogService, diff --git a/src/vs/workbench/services/editor/common/editorOpenWith.ts b/src/vs/workbench/services/editor/common/editorOpenWith.ts index af8462e5f42..a824ac2e408 100644 --- a/src/vs/workbench/services/editor/common/editorOpenWith.ts +++ b/src/vs/workbench/services/editor/common/editorOpenWith.ts @@ -49,7 +49,12 @@ export async function openEditorWith( return; } - const overrideToUse = typeof id === 'string' && allEditorOverrides.find(([_, entry]) => entry.id === id); + let overrideToUse; + if (typeof id === 'string') { + overrideToUse = allEditorOverrides.find(([_, entry]) => entry.id === id); + } else if (allEditorOverrides.length === 1) { + overrideToUse = allEditorOverrides[0]; + } if (overrideToUse) { return overrideToUse[0].open(input, overrideOptions, group, OpenEditorContext.NEW_EDITOR)?.override; } diff --git a/src/vs/workbench/services/experiment/common/experimentService.ts b/src/vs/workbench/services/experiment/common/experimentService.ts new file mode 100644 index 00000000000..758c525e263 --- /dev/null +++ b/src/vs/workbench/services/experiment/common/experimentService.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const ITASExperimentService = createDecorator('TASExperimentService'); + +export interface ITASExperimentService { + readonly _serviceBrand: undefined; + getTreatment(name: string): Promise; +} diff --git a/src/vs/workbench/services/experiment/electron-browser/experimentService.ts b/src/vs/workbench/services/experiment/electron-browser/experimentService.ts new file mode 100644 index 00000000000..8a5016a8f51 --- /dev/null +++ b/src/vs/workbench/services/experiment/electron-browser/experimentService.ts @@ -0,0 +1,227 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as platform from 'vs/base/common/platform'; +import { 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'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +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 = 0; // no polling + +class MementoKeyValueStorage implements IKeyValueStorage { + constructor(private mementoObj: MementoObject) { } + + async getValue(key: string, defaultValue?: T | undefined): Promise { + const value = await this.mementoObj[key]; + return value || defaultValue; + } + + setValue(key: string, value: T): void { + this.mementoObj[key] = value; + } +} + +class ExperimentServiceTelemetry implements IExperimentationTelemetry { + constructor(private telemetryService: ITelemetryService) { } + + // __GDPR__COMMON__ "VSCode.ABExp.Features" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + // __GDPR__COMMON__ "abexp.assignmentcontext" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + setSharedProperty(name: string, value: string): void { + this.telemetryService.setExperimentProperty(name, value); + } + + postEvent(eventName: string, props: Map): void { + const data: ITelemetryData = {}; + for (const [key, value] of props.entries()) { + data[key] = value; + } + + /* __GDPR__ + "query-expfeature" : { + "ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog(eventName, data); + } +} + +class ExperimentServiceFilterProvider implements IExperimentationFilterProvider { + constructor( + private version: string, + private appName: string, + private machineId: string, + private targetPopulation: TargetPopulation + ) { } + + getFilterValue(filter: string): string | null { + switch (filter) { + case Filters.ApplicationVersion: + return this.version; // productService.version + case Filters.Build: + return this.appName; // productService.nameLong + case Filters.ClientId: + return this.machineId; + case Filters.Language: + return platform.language; + case Filters.ExtensionName: + return 'vscode-core'; // always return vscode-core for exp service + case Filters.TargetPopulation: + return this.targetPopulation; + default: + return ''; + } + } + + getFilters(): Map { + let filters: Map = new Map(); + let filterValues = Object.values(Filters); + for (let value of filterValues) { + filters.set(value, this.getFilterValue(value)); + } + + return filters; + } +} + +/* +Based upon the official VSCode currently existing filters in the +ExP backend for the VSCode cluster. +https://experimentation.visualstudio.com/Analysis%20and%20Experimentation/_git/AnE.ExP.TAS.TachyonHost.Configuration?path=%2FConfigurations%2Fvscode%2Fvscode.json&version=GBmaster +"X-MSEdge-Market": "detection.market", +"X-FD-Corpnet": "detection.corpnet", +"X-VSCode–AppVersion": "appversion", +"X-VSCode-Build": "build", +"X-MSEdge-ClientId": "clientid", +"X-VSCode-ExtensionName": "extensionname", +"X-VSCode-TargetPopulation": "targetpopulation", +"X-VSCode-Language": "language" +*/ + +enum Filters { + /** + * The market in which the extension is distributed. + */ + Market = 'X-MSEdge-Market', + + /** + * The corporation network. + */ + CorpNet = 'X-FD-Corpnet', + + /** + * Version of the application which uses experimentation service. + */ + ApplicationVersion = 'X-VSCode-AppVersion', + + /** + * Insiders vs Stable. + */ + Build = 'X-VSCode-Build', + + /** + * Client Id which is used as primary unit for the experimentation. + */ + ClientId = 'X-MSEdge-ClientId', + + /** + * Extension header. + */ + ExtensionName = 'X-VSCode-ExtensionName', + + /** + * The language in use by VS Code + */ + Language = 'X-VSCode-Language', + + /** + * The target population. + * This is used to separate internal, early preview, GA, etc. + */ + TargetPopulation = 'X-VSCode-TargetPopulation', +} + +enum TargetPopulation { + Team = 'team', + Internal = 'internal', + Insiders = 'insider', + Public = 'public', +} + +export class ExperimentService implements ITASExperimentService { + _serviceBrand: undefined; + 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, + @IConfigurationService private configurationService: IConfigurationService, + ) { + + if (this.productService.tasConfig && this.experimentsEnabled) { + this.tasClient = this.setupTASClient(); + } + } + + async getTreatment(name: string): Promise { + if (!this.tasClient) { + return undefined; + } + + if (!this.experimentsEnabled) { + return undefined; + } + + return (await this.tasClient).getTreatmentVariable('vscode', name); + } + + private async setupTASClient(): Promise { + const telemetryInfo = await this.telemetryService.getTelemetryInfo(); + const targetPopulation = telemetryInfo.msftInternal ? TargetPopulation.Internal : (this.productService.quality === 'stable' ? TargetPopulation.Public : TargetPopulation.Insiders); + const machineId = telemetryInfo.machineId; + const filterProvider = new ExperimentServiceFilterProvider( + this.productService.version, + this.productService.nameLong, + machineId, + targetPopulation + ); + + const memento = new Memento(ExperimentService.MEMENTO_ID, this.storageService); + const keyValueStorage = new MementoKeyValueStorage(memento.getMemento(StorageScope.GLOBAL)); + + const telemetry = new ExperimentServiceTelemetry(this.telemetryService); + + const tasConfig = this.productService.tasConfig!; + const tasClient = new TASClient({ + filterProviders: [filterProvider], + telemetry: telemetry, + storageKey: storageKey, + keyValueStorage: keyValueStorage, + featuresTelemetryPropertyName: tasConfig.featuresTelemetryPropertyName, + assignmentContextTelemetryPropertyName: tasConfig.assignmentContextTelemetryPropertyName, + telemetryEventName: tasConfig.telemetryEventName, + endpoint: tasConfig.endpoint, + refetchInterval: refetchInterval, + }); + + await tasClient.initializePromise; + return tasClient; + } +} + +registerSingleton(ITASExperimentService, ExperimentService, false); + diff --git a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts index 7790f5b83a5..60940f44d2d 100644 --- a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts @@ -12,11 +12,11 @@ import { URI } from 'vs/base/common/uri'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; interface IScannedBuiltinExtension { - extensionPath: string, - packageJSON: IExtensionManifest, - packageNLSPath?: string, - readmePath?: string, - changelogPath?: string, + extensionPath: string; + packageJSON: IExtensionManifest; + packageNLS?: any; + readmePath?: string; + changelogPath?: string; } export class BuiltinExtensionsScannerService implements IBuiltinExtensionsScannerService { @@ -29,38 +29,54 @@ export class BuiltinExtensionsScannerService implements IBuiltinExtensionsScanne @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IUriIdentityService uriIdentityService: IUriIdentityService, ) { + if (isWeb) { + const builtinExtensionsServiceUrl = this._getBuiltinExtensionsUrl(environmentService); + if (builtinExtensionsServiceUrl) { + let scannedBuiltinExtensions: IScannedBuiltinExtension[] = []; - const builtinExtensionsServiceUrl = environmentService.options?.builtinExtensionsServiceUrl ? URI.parse(environmentService.options?.builtinExtensionsServiceUrl) : undefined; - if (isWeb && builtinExtensionsServiceUrl) { - - let scannedBuiltinExtensions: IScannedBuiltinExtension[] = []; - - if (environmentService.isBuilt) { - // Built time configuration (do NOT modify) - scannedBuiltinExtensions = [/*BUILD->INSERT_BUILTIN_EXTENSIONS*/]; - } else { - // Find builtin extensions by checking for DOM - const builtinExtensionsElement = document.getElementById('vscode-workbench-builtin-extensions'); - const builtinExtensionsElementAttribute = builtinExtensionsElement ? builtinExtensionsElement.getAttribute('data-settings') : undefined; - if (builtinExtensionsElementAttribute) { - try { - scannedBuiltinExtensions = JSON.parse(builtinExtensionsElementAttribute); - } catch (error) { /* ignore error*/ } + if (environmentService.isBuilt) { + // Built time configuration (do NOT modify) + scannedBuiltinExtensions = [/*BUILD->INSERT_BUILTIN_EXTENSIONS*/]; + } else { + // Find builtin extensions by checking for DOM + const builtinExtensionsElement = document.getElementById('vscode-workbench-builtin-extensions'); + const builtinExtensionsElementAttribute = builtinExtensionsElement ? builtinExtensionsElement.getAttribute('data-settings') : undefined; + if (builtinExtensionsElementAttribute) { + try { + scannedBuiltinExtensions = JSON.parse(builtinExtensionsElementAttribute); + } catch (error) { /* ignore error*/ } + } } - } - this.builtinExtensions = scannedBuiltinExtensions.map(e => ({ - identifier: { id: getGalleryExtensionId(e.packageJSON.publisher, e.packageJSON.name) }, - location: uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.extensionPath), - type: ExtensionType.System, - packageJSON: e.packageJSON, - packageNLSUrl: e.packageNLSPath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.packageNLSPath) : undefined, - readmeUrl: e.readmePath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.readmePath) : undefined, - changelogUrl: e.changelogPath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.changelogPath) : undefined, - })); + this.builtinExtensions = scannedBuiltinExtensions.map(e => ({ + identifier: { id: getGalleryExtensionId(e.packageJSON.publisher, e.packageJSON.name) }, + location: uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.extensionPath), + type: ExtensionType.System, + packageJSON: e.packageJSON, + packageNLS: e.packageNLS, + readmeUrl: e.readmePath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.readmePath) : undefined, + changelogUrl: e.changelogPath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.changelogPath) : undefined, + })); + } } } + private _getBuiltinExtensionsUrl(environmentService: IWorkbenchEnvironmentService): URI | undefined { + if (environmentService.options?.builtinExtensionsServiceUrl) { + return URI.parse(environmentService.options?.builtinExtensionsServiceUrl); + } + let enableBuiltinExtensions: boolean; + if (environmentService.options && typeof environmentService.options._enableBuiltinExtensions !== 'undefined') { + enableBuiltinExtensions = environmentService.options._enableBuiltinExtensions; + } else { + enableBuiltinExtensions = environmentService.configuration.remoteAuthority ? false : true; + } + if (enableBuiltinExtensions) { + return URI.parse(require.toUrl('../../../../../../extensions')); + } + return undefined; + } + async scanBuiltinExtensions(): Promise { if (isWeb) { return this.builtinExtensions; diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 8adc6b0fb3b..34647803e9b 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -6,7 +6,7 @@ import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; -import { IExtension, IScannedExtension, ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { IExtension, IScannedExtension, ExtensionType, ITranslatedScannedExtension } from 'vs/platform/extensions/common/extensions'; import { IExtensionManagementService, IGalleryExtension, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkspace, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IStringDictionary } from 'vs/base/common/collections'; @@ -128,6 +128,7 @@ export interface IExtensionRecommendationsService { getAllRecommendationsWithReason(): IStringDictionary; getFileBasedRecommendations(): IExtensionRecommendation[]; + getImportantRecommendations(): Promise; getConfigBasedRecommendations(): Promise; getOtherRecommendations(): Promise; getWorkspaceRecommendations(): Promise; @@ -142,6 +143,7 @@ export const IWebExtensionsScannerService = createDecorator; + scanAndTranslateExtensions(type?: ExtensionType): Promise; addExtension(galleryExtension: IGalleryExtension): Promise; removeExtension(identifier: IExtensionIdentifier, version?: string): Promise; } diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index 132e3d4e018..98a423df368 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -3,13 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionType, IExtensionIdentifier, IExtensionManifest, IScannedExtension } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, IExtensionIdentifier, IExtensionManifest, ITranslatedScannedExtension } from 'vs/platform/extensions/common/extensions'; import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, IGalleryExtension, IReportedExtension, IGalleryMetadata, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { IRequestService, isSuccess, asText } from 'vs/platform/request/common/request'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ILogService } from 'vs/platform/log/common/log'; @@ -33,14 +30,13 @@ export class WebExtensionManagementService extends Disposable implements IExtens constructor( @IWebExtensionsScannerService private readonly webExtensionsScannerService: IWebExtensionsScannerService, - @IRequestService private readonly requestService: IRequestService, @ILogService private readonly logService: ILogService, ) { super(); } async getInstalled(type?: ExtensionType): Promise { - const extensions = await this.webExtensionsScannerService.scanExtensions(type); + const extensions = await this.webExtensionsScannerService.scanAndTranslateExtensions(type); return Promise.all(extensions.map(e => this.toLocalExtension(e))); } @@ -83,23 +79,11 @@ export class WebExtensionManagementService extends Disposable implements IExtens return userExtensions.find(e => areSameExtensions(e.identifier, identifier)); } - private async toLocalExtension(scannedExtension: IScannedExtension): Promise { - let manifest = scannedExtension.packageJSON; - if (scannedExtension.packageNLSUrl) { - try { - const context = await this.requestService.request({ type: 'GET', url: scannedExtension.packageNLSUrl.toString() }, CancellationToken.None); - if (isSuccess(context)) { - const content = await asText(context); - if (content) { - manifest = localizeManifest(manifest, JSON.parse(content)); - } - } - } catch (error) { /* ignore */ } - } + private async toLocalExtension(scannedExtension: ITranslatedScannedExtension): Promise { return { type: scannedExtension.type, identifier: scannedExtension.identifier, - manifest, + manifest: scannedExtension.packageJSON, location: scannedExtension.location, isMachineScoped: false, publisherId: null, diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts index d78328501c9..2faf33d9c22 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as semver from 'semver-umd'; -import { IBuiltinExtensionsScannerService, IScannedExtension, ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +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'; import { isWeb } from 'vs/base/common/platform'; @@ -21,6 +21,9 @@ import { IGalleryExtension } from 'vs/platform/extensionManagement/common/extens import { groupByExtension, areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStaticExtension } from 'vs/workbench/workbench.web.api'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; +import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; interface IUserExtension { identifier: IExtensionIdentifier; @@ -44,7 +47,7 @@ const AssetTypeWebResource = 'Microsoft.VisualStudio.Code.WebResources'; function getExtensionLocation(assetUri: URI): URI { return joinPath(assetUri, AssetTypeWebResource, 'extension'); } -export class WebExtensionsScannerService implements IWebExtensionsScannerService { +export class WebExtensionsScannerService extends Disposable implements IWebExtensionsScannerService { declare readonly _serviceBrand: undefined; @@ -53,6 +56,8 @@ export class WebExtensionsScannerService implements IWebExtensionsScannerService private readonly extensionsResource: URI | undefined = undefined; private readonly userExtensionsResourceLimiter: Queue = new Queue(); + private userExtensionsPromise: Promise | undefined; + constructor( @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IBuiltinExtensionsScannerService private readonly builtinExtensionsScannerService: IBuiltinExtensionsScannerService, @@ -61,22 +66,48 @@ export class WebExtensionsScannerService implements IWebExtensionsScannerService @ILogService private readonly logService: ILogService, @IConfigurationService private readonly configurationService: IConfigurationService, ) { + super(); if (isWeb) { this.extensionsResource = joinPath(environmentService.userRoamingDataHome, 'extensions.json'); - this.systemExtensionsPromise = this.builtinExtensionsScannerService.scanBuiltinExtensions(); + this.systemExtensionsPromise = this.readSystemExtensions(); this.defaultExtensionsPromise = this.readDefaultExtensions(); + if (this.extensionsResource) { + this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.extensionsResource!))(() => this.userExtensionsPromise = undefined)); + } } } - private async readDefaultExtensions(): Promise { + private async readSystemExtensions(): Promise { + const extensions = await this.builtinExtensionsScannerService.scanBuiltinExtensions(); + return extensions.concat(this.getStaticExtensions(true)); + } + + /** + * All extensions defined via `staticExtensions` + */ + private getStaticExtensions(builtin: boolean): IScannedExtension[] { const staticExtensions = this.environmentService.options && Array.isArray(this.environmentService.options.staticExtensions) ? this.environmentService.options.staticExtensions : []; + return ( + staticExtensions + .filter(e => Boolean(e.isBuiltin) === builtin) + .map(e => ({ + identifier: { id: getGalleryExtensionId(e.packageJSON.publisher, e.packageJSON.name) }, + location: e.extensionLocation, + type: e.isBuiltin ? ExtensionType.System : ExtensionType.User, + packageJSON: e.packageJSON, + })) + ); + } + + private async readDefaultExtensions(): Promise { const defaultUserWebExtensions = await this.readDefaultUserWebExtensions(); - return [...staticExtensions, ...defaultUserWebExtensions].map(e => ({ + const extensions = defaultUserWebExtensions.map(e => ({ identifier: { id: getGalleryExtensionId(e.packageJSON.publisher, e.packageJSON.name) }, location: e.extensionLocation, type: ExtensionType.User, packageJSON: e.packageJSON, })); + return extensions.concat(this.getStaticExtensions(false)); } private async readDefaultUserWebExtensions(): Promise { @@ -113,12 +144,52 @@ export class WebExtensionsScannerService implements IWebExtensionsScannerService if (type === undefined || type === ExtensionType.User) { const staticExtensions = await this.defaultExtensionsPromise; extensions.push(...staticExtensions); - const userExtensions = await this.scanUserExtensions(); + if (!this.userExtensionsPromise) { + this.userExtensionsPromise = this.scanUserExtensions(); + } + const userExtensions = await this.userExtensionsPromise; extensions.push(...userExtensions); } return extensions; } + async scanAndTranslateExtensions(type?: ExtensionType): Promise { + const extensions = await this.scanExtensions(type); + return Promise.all(extensions.map((ext) => this._translateScannedExtension(ext))); + } + + private async _translateScannedExtension(scannedExtension: IScannedExtension): Promise { + let manifest = scannedExtension.packageJSON; + if (scannedExtension.packageNLS) { + // package.nls.json is inlined + try { + manifest = localizeManifest(manifest, scannedExtension.packageNLS); + } catch (error) { + console.log(error); + /* ignore */ + } + } else if (scannedExtension.packageNLSUrl) { + // package.nls.json needs to be fetched + try { + const context = await this.requestService.request({ type: 'GET', url: scannedExtension.packageNLSUrl.toString() }, CancellationToken.None); + if (isSuccess(context)) { + const content = await asText(context); + if (content) { + manifest = localizeManifest(manifest, JSON.parse(content)); + } + } + } catch (error) { /* ignore */ } + } + return { + identifier: scannedExtension.identifier, + location: scannedExtension.location, + type: scannedExtension.type, + packageJSON: manifest, + readmeUrl: scannedExtension.readmeUrl, + changelogUrl: scannedExtension.changelogUrl + }; + } + async addExtension(galleryExtension: IGalleryExtension): Promise { if (!galleryExtension.assetTypes.some(type => type.startsWith(AssetTypeWebResource))) { throw new Error(`Missing ${AssetTypeWebResource} asset type`); @@ -149,7 +220,7 @@ export class WebExtensionsScannerService implements IWebExtensionsScannerService async removeExtension(identifier: IExtensionIdentifier, version?: string): Promise { let userExtensions = await this.readUserExtensions(); - userExtensions = userExtensions.filter(extension => !(areSameExtensions(extension.identifier, identifier) && version ? extension.version === version : true)); + userExtensions = userExtensions.filter(extension => !(areSameExtensions(extension.identifier, identifier) && (version ? extension.version === version : true))); await this.writeUserExtensions(userExtensions); } @@ -226,6 +297,7 @@ export class WebExtensionsScannerService implements IWebExtensionsScannerService packageNLSUri: e.packageNLSUri?.toJSON(), })); await this.fileService.writeFile(this.extensionsResource!, VSBuffer.fromString(JSON.stringify(storedUserExtensions))); + this.userExtensionsPromise = undefined; return userExtensions; }); } 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 4b26efed3eb..d7ccb3fafec 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -397,7 +397,7 @@ suite('ExtensionEnablementService Test', () => { assert.ok(!testObject.canChangeEnablement(extension)); }); - test('test extension is disabled when disabled in enviroment', async () => { + test('test extension is disabled when disabled in environment', async () => { const extension = aLocalExtension('pub.a'); instantiationService.stub(IWorkbenchEnvironmentService, { disableExtensions: ['pub.a'] } as IWorkbenchEnvironmentService); instantiationService.stub(IExtensionManagementService, { onDidUninstallExtension: didUninstallEvent.event, getInstalled: () => Promise.resolve([extension, aLocalExtension('pub.b')]) } as IExtensionManagementService); diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index df204c0b0a9..d0710e77fa2 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -17,19 +17,19 @@ import { AbstractExtensionService, parseScannedExtension } from 'vs/workbench/se import { RemoteExtensionHost, IRemoteExtensionHostDataProvider, IRemoteExtensionHostInitData } from 'vs/workbench/services/extensions/common/remoteExtensionHost'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost'; -import { canExecuteOnWeb } from 'vs/workbench/services/extensions/common/extensionsUtil'; +import { getExtensionKind } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription, ExtensionKind } from 'vs/platform/extensions/common/extensions'; import { FetchFileSystemProvider } from 'vs/workbench/services/extensions/browser/webWorkerFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { DeltaExtensionsResult } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; export class ExtensionService extends AbstractExtensionService implements IExtensionService { private _disposables = new DisposableStore(); private _remoteInitData: IRemoteExtensionHostInitData | null = null; + private _runningLocation: Map; constructor( @IInstantiationService instantiationService: IInstantiationService, @@ -54,6 +54,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten productService, ); + this._runningLocation = new Map(); + this._initialize(); this._initFetchFileSystem(); } @@ -73,10 +75,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten return { getInitData: async () => { const allExtensions = await this.getExtensions(); - const webExtensions = allExtensions.filter(ext => canExecuteOnWeb(ext, this._productService, this._configService)); + const localWebWorkerExtensions = filterByRunningLocation(allExtensions, this._runningLocation, ExtensionRunningLocation.LocalWebWorker); return { autoStart: true, - extensions: webExtensions + extensions: localWebWorkerExtensions }; } }; @@ -109,32 +111,26 @@ export class ExtensionService extends AbstractExtensionService implements IExten protected async _scanAndHandleExtensions(): Promise { // fetch the remote environment - let [remoteEnv, localExtensions] = await Promise.all([ + let [localExtensions, remoteEnv, remoteExtensions] = await Promise.all([ + this._webExtensionsScannerService.scanAndTranslateExtensions().then(extensions => extensions.map(parseScannedExtension)), this._remoteAgentService.getEnvironment(), - this._webExtensionsScannerService.scanExtensions().then(extensions => extensions.map(parseScannedExtension)) + this._remoteAgentService.scanExtensions() ]); + localExtensions = this._checkEnabledAndProposedAPI(localExtensions); + remoteExtensions = this._checkEnabledAndProposedAPI(remoteExtensions); const remoteAgentConnection = this._remoteAgentService.getConnection(); + this._runningLocation = _determineRunningLocation(this._productService, this._configService, localExtensions, remoteExtensions, Boolean(remoteEnv && remoteAgentConnection)); - let result: DeltaExtensionsResult; + localExtensions = filterByRunningLocation(localExtensions, this._runningLocation, ExtensionRunningLocation.LocalWebWorker); + remoteExtensions = filterByRunningLocation(remoteExtensions, this._runningLocation, ExtensionRunningLocation.Remote); - // local: only enabled and web'ish extension - localExtensions = localExtensions!.filter(ext => this._isEnabled(ext) && canExecuteOnWeb(ext, this._productService, this._configService)); - this._checkEnableProposedApi(localExtensions); - - if (!remoteEnv || !remoteAgentConnection) { - result = this._registry.deltaExtensions(localExtensions, []); - - } else { - // remote: only enabled and none-web'ish extension - remoteEnv.extensions = remoteEnv.extensions.filter(extension => this._isEnabled(extension) && !canExecuteOnWeb(extension, this._productService, this._configService)); - this._checkEnableProposedApi(remoteEnv.extensions); - - // in case of overlap, the remote wins - const isRemoteExtension = new Set(); - remoteEnv.extensions.forEach(extension => isRemoteExtension.add(ExtensionIdentifier.toKey(extension.identifier))); - localExtensions = localExtensions.filter(extension => !isRemoteExtension.has(ExtensionIdentifier.toKey(extension.identifier))); + const result = this._registry.deltaExtensions(remoteExtensions.concat(localExtensions), []); + if (result.removedDueToLooping.length > 0) { + this._logOrShowMessage(Severity.Error, nls.localize('looping', "The following extensions contain dependency loops and have been disabled: {0}", result.removedDueToLooping.map(e => `'${e.identifier.value}'`).join(', '))); + } + if (remoteEnv && remoteAgentConnection) { // save for remote extension's init data this._remoteInitData = { connectionData: this._remoteAuthorityResolverService.getConnectionData(remoteAgentConnection.remoteAuthority), @@ -143,16 +139,11 @@ export class ExtensionService extends AbstractExtensionService implements IExten extensionHostLogsPath: remoteEnv.extensionHostLogsPath, globalStorageHome: remoteEnv.globalStorageHome, workspaceStorageHome: remoteEnv.workspaceStorageHome, - extensions: remoteEnv.extensions, - allExtensions: remoteEnv.extensions.concat(localExtensions) + extensions: remoteExtensions, + allExtensions: this._registry.getAllExtensionDescriptions() }; - - result = this._registry.deltaExtensions(remoteEnv.extensions.concat(localExtensions), []); } - if (result.removedDueToLooping.length > 0) { - this._logOrShowMessage(Severity.Error, nls.localize('looping', "The following extensions contain dependency loops and have been disabled: {0}", result.removedDueToLooping.map(e => `'${e.identifier.value}'`).join(', '))); - } this._doHandleExtensionPoints(this._registry.getAllExtensionDescriptions()); } @@ -164,4 +155,55 @@ export class ExtensionService extends AbstractExtensionService implements IExten } } +const enum ExtensionRunningLocation { + None, + LocalWebWorker, + Remote +} + +export function determineRunningLocation(localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[], allExtensionKinds: Map, hasRemote: boolean): Map { + const localExtensionsSet = new Set(); + localExtensions.forEach(ext => localExtensionsSet.add(ExtensionIdentifier.toKey(ext.identifier))); + + const remoteExtensionsSet = new Set(); + remoteExtensions.forEach(ext => remoteExtensionsSet.add(ExtensionIdentifier.toKey(ext.identifier))); + + const pickRunningLocation = (extension: IExtensionDescription): ExtensionRunningLocation => { + const isInstalledLocally = localExtensionsSet.has(ExtensionIdentifier.toKey(extension.identifier)); + const isInstalledRemotely = remoteExtensionsSet.has(ExtensionIdentifier.toKey(extension.identifier)); + const extensionKinds = allExtensionKinds.get(ExtensionIdentifier.toKey(extension.identifier)) || []; + for (const extensionKind of extensionKinds) { + if (extensionKind === 'ui' && isInstalledRemotely) { + // ui extensions run remotely if possible + return ExtensionRunningLocation.Remote; + } + if (extensionKind === 'workspace' && isInstalledRemotely) { + // workspace extensions run remotely if possible + return ExtensionRunningLocation.Remote; + } + if (extensionKind === 'web' && isInstalledLocally) { + // web worker extensions run in the local web worker if possible + return ExtensionRunningLocation.LocalWebWorker; + } + } + return ExtensionRunningLocation.None; + }; + + const runningLocation = new Map(); + localExtensions.forEach(ext => runningLocation.set(ExtensionIdentifier.toKey(ext.identifier), pickRunningLocation(ext))); + remoteExtensions.forEach(ext => runningLocation.set(ExtensionIdentifier.toKey(ext.identifier), pickRunningLocation(ext))); + return runningLocation; +} + +function _determineRunningLocation(productService: IProductService, configurationService: IConfigurationService, localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[], hasRemote: boolean): Map { + const allExtensionKinds = new Map(); + localExtensions.forEach(ext => allExtensionKinds.set(ExtensionIdentifier.toKey(ext.identifier), getExtensionKind(ext, productService, configurationService))); + remoteExtensions.forEach(ext => allExtensionKinds.set(ExtensionIdentifier.toKey(ext.identifier), getExtensionKind(ext, productService, configurationService))); + return determineRunningLocation(localExtensions, remoteExtensions, allExtensionKinds, hasRemote); +} + +function filterByRunningLocation(extensions: IExtensionDescription[], runningLocation: Map, desiredRunningLocation: ExtensionRunningLocation): IExtensionDescription[] { + return extensions.filter(ext => runningLocation.get(ExtensionIdentifier.toKey(ext.identifier)) === desiredRunningLocation); +} + registerSingleton(IExtensionService, ExtensionService); diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index 031ed230b86..6b672330aba 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -5,7 +5,7 @@ import { getWorkerBootstrapUrl } from 'vs/base/worker/defaultWorkerFactory'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { toDisposable, Disposable } from 'vs/base/common/lifecycle'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { VSBuffer } from 'vs/base/common/buffer'; import { createMessageOfType, MessageType, isMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; @@ -16,6 +16,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import * as platform from 'vs/base/common/platform'; +import * as dom from 'vs/base/browser/dom'; import { URI } from 'vs/base/common/uri'; import { IExtensionHost, ExtensionHostLogFileName, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -24,6 +25,9 @@ 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 { localize } from 'vs/nls'; +import { generateUuid } from 'vs/base/common/uuid'; +import { canceled, onUnexpectedError } from 'vs/base/common/errors'; +import { WEB_WORKER_IFRAME } from 'vs/workbench/services/extensions/common/webWorkerIframe'; export interface IWebWorkerExtensionHostInitData { readonly autoStart: boolean; @@ -34,17 +38,17 @@ export interface IWebWorkerExtensionHostDataProvider { getInitData(): Promise; } -export class WebWorkerExtensionHost implements IExtensionHost { +export class WebWorkerExtensionHost extends Disposable implements IExtensionHost { public readonly kind = ExtensionHostKind.LocalWebWorker; public readonly remoteAuthority = null; - private _toDispose = new DisposableStore(); - private _isTerminating: boolean = false; - private _protocol?: IMessagePassingProtocol; + private readonly _onDidExit = this._register(new Emitter<[number, string | null]>()); + public readonly onExit: Event<[number, string | null]> = this._onDidExit.event; - private readonly _onDidExit = new Emitter<[number, string | null]>(); - readonly onExit: Event<[number, string | null]> = this._onDidExit.event; + private _isTerminating: boolean; + private _protocolPromise: Promise | null; + private _protocol: IMessagePassingProtocol | null; private readonly _extensionHostLogsLocation: URI; private readonly _extensionHostLogFile: URI; @@ -58,76 +62,169 @@ export class WebWorkerExtensionHost implements IExtensionHost { @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @IProductService private readonly _productService: IProductService, ) { + super(); + this._isTerminating = false; + this._protocolPromise = null; + this._protocol = null; this._extensionHostLogsLocation = URI.file(this._environmentService.logsPath).with({ scheme: this._environmentService.logFile.scheme }); this._extensionHostLogFile = joinPath(this._extensionHostLogsLocation, `${ExtensionHostLogFileName}.log`); } - async start(): Promise { - - if (!this._protocol) { - - const emitter = new Emitter(); - - const url = getWorkerBootstrapUrl(require.toUrl('../worker/extensionHostWorkerMain.js'), 'WorkerExtensionHost'); - const worker = new Worker(url, { name: 'WorkerExtensionHost' }); - - worker.onmessage = (event) => { - const { data } = event; - if (!(data instanceof ArrayBuffer)) { - console.warn('UNKNOWN data received', data); - this._onDidExit.fire([77, 'UNKNOWN data received']); - return; - } - - emitter.fire(VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength))); - }; - - worker.onerror = (event) => { - console.error(event.message, event.error); - this._onDidExit.fire([81, event.message || event.error]); - }; - - // keep for cleanup - this._toDispose.add(emitter); - this._toDispose.add(toDisposable(() => worker.terminate())); - - const protocol: IMessagePassingProtocol = { - onMessage: emitter.event, - send: vsbuf => { - const data = vsbuf.buffer.buffer.slice(vsbuf.buffer.byteOffset, vsbuf.buffer.byteOffset + vsbuf.buffer.byteLength); - worker.postMessage(data, [data]); - } - }; - - // extension host handshake happens below - // (1) <== wait for: Ready - // (2) ==> send: init data - // (3) <== wait for: Initialized - - await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Ready))); - protocol.send(VSBuffer.fromString(JSON.stringify(await this._createExtHostInitData()))); - await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Initialized))); - - // Register log channel for web worker exthost log - Registry.as(Extensions.OutputChannels).registerChannel({ id: 'webWorkerExtHostLog', label: localize('name', "Worker Extension Host"), file: this._extensionHostLogFile, log: true }); - - this._protocol = protocol; + public async start(): Promise { + if (!this._protocolPromise) { + if (platform.isWeb && this._environmentService.options && this._environmentService.options._wrapWebWorkerExtHostInIframe) { + this._protocolPromise = this._startInsideIframe(); + } else { + this._protocolPromise = this._startOutsideIframe(); + } + this._protocolPromise.then(protocol => this._protocol = protocol); } - return this._protocol; - + return this._protocolPromise; } - dispose(): void { - if (!this._protocol) { - this._toDispose.dispose(); - return; + private _startInsideIframe(): Promise { + const emitter = this._register(new Emitter()); + + const iframe = document.createElement('iframe'); + iframe.setAttribute('class', 'web-worker-ext-host-iframe'); + iframe.setAttribute('sandbox', 'allow-scripts'); + iframe.style.display = 'none'; + + const vscodeWebWorkerExtHostId = generateUuid(); + const workerUrl = require.toUrl('../worker/extensionHostWorkerMain.js'); + const workerSrc = getWorkerBootstrapUrl(workerUrl, 'WorkerExtensionHost', true); + const escapeAttribute = (value: string): string => { + return value.replace(/"/g, '"'); + }; + const isBuilt = this._environmentService.isBuilt; + const html = ` + + + + + + + + + +`; + const iframeContent = `data:text/html;charset=utf-8,${encodeURIComponent(html)}`; + iframe.setAttribute('src', iframeContent); + + this._register(dom.addDisposableListener(window, 'message', (event) => { + if (event.source !== iframe.contentWindow) { + return; + } + if (event.data.vscodeWebWorkerExtHostId !== vscodeWebWorkerExtHostId) { + return; + } + if (event.data.error) { + const { name, message, stack } = event.data.error; + const err = new Error(); + err.message = message; + err.name = name; + err.stack = stack; + onUnexpectedError(err); + this._onDidExit.fire([18, err.message]); + return; + } + const { data } = event.data; + if (!(data instanceof ArrayBuffer)) { + console.warn('UNKNOWN data received', data); + this._onDidExit.fire([77, 'UNKNOWN data received']); + return; + } + emitter.fire(VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength))); + })); + + const protocol: IMessagePassingProtocol = { + onMessage: emitter.event, + send: vsbuf => { + const data = vsbuf.buffer.buffer.slice(vsbuf.buffer.byteOffset, vsbuf.buffer.byteOffset + vsbuf.buffer.byteLength); + iframe.contentWindow!.postMessage({ + vscodeWebWorkerExtHostId, + data: data + }, '*', [data]); + } + }; + + document.body.appendChild(iframe); + this._register(toDisposable(() => iframe.remove())); + + return this._performHandshake(protocol); + } + + private _startOutsideIframe(): Promise { + const emitter = new Emitter(); + + const url = getWorkerBootstrapUrl(require.toUrl('../worker/extensionHostWorkerMain.js'), 'WorkerExtensionHost'); + const worker = new Worker(url, { name: 'WorkerExtensionHost' }); + + worker.onmessage = (event) => { + const { data } = event; + if (!(data instanceof ArrayBuffer)) { + console.warn('UNKNOWN data received', data); + this._onDidExit.fire([77, 'UNKNOWN data received']); + return; + } + + emitter.fire(VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength))); + }; + + worker.onerror = (event) => { + console.error(event.message, event.error); + this._onDidExit.fire([81, event.message || event.error]); + }; + + // keep for cleanup + this._register(emitter); + this._register(toDisposable(() => worker.terminate())); + + const protocol: IMessagePassingProtocol = { + onMessage: emitter.event, + send: vsbuf => { + const data = vsbuf.buffer.buffer.slice(vsbuf.buffer.byteOffset, vsbuf.buffer.byteOffset + vsbuf.buffer.byteLength); + worker.postMessage(data, [data]); + } + }; + + return this._performHandshake(protocol); + } + + private async _performHandshake(protocol: IMessagePassingProtocol): Promise { + // extension host handshake happens below + // (1) <== wait for: Ready + // (2) ==> send: init data + // (3) <== wait for: Initialized + + await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Ready))); + if (this._isTerminating) { + throw canceled(); } + protocol.send(VSBuffer.fromString(JSON.stringify(await this._createExtHostInitData()))); + if (this._isTerminating) { + throw canceled(); + } + await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Initialized))); + if (this._isTerminating) { + throw canceled(); + } + + // Register log channel for web worker exthost log + Registry.as(Extensions.OutputChannels).registerChannel({ id: 'webWorkerExtHostLog', label: localize('name', "Worker Extension Host"), file: this._extensionHostLogFile, log: true }); + + return protocol; + } + + public dispose(): void { if (this._isTerminating) { return; } this._isTerminating = true; - this._protocol.send(createMessageOfType(MessageType.Terminate)); - setTimeout(() => this._toDispose.dispose(), 10 * 1000); + if (this._protocol) { + this._protocol.send(createMessageOfType(MessageType.Terminate)); + } + super.dispose(); } getInspectPort(): number | undefined { diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 2fabfc1b08c..446f74eff84 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -20,7 +20,7 @@ import { ExtensionMessageCollector, ExtensionPoint, ExtensionsRegistry, IExtensi import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { ResponsiveState } from 'vs/workbench/services/extensions/common/rpcProtocol'; import { ExtensionHostManager } from 'vs/workbench/services/extensions/common/extensionHostManager'; -import { ExtensionIdentifier, IExtensionDescription, IScannedExtension, ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription, ExtensionType, ITranslatedScannedExtension } from 'vs/platform/extensions/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -29,7 +29,7 @@ import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtens const hasOwnProperty = Object.hasOwnProperty; const NO_OP_VOID_PROMISE = Promise.resolve(undefined); -export function parseScannedExtension(extension: IScannedExtension): IExtensionDescription { +export function parseScannedExtension(extension: ITranslatedScannedExtension): IExtensionDescription { return { identifier: new ExtensionIdentifier(`${extension.packageJSON.publisher}.${extension.packageJSON.name}`), isBuiltin: extension.type === ExtensionType.System, diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index 9d05e123c8c..484444e968d 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -27,7 +27,6 @@ import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtens // Enable to see detailed message communication between window and extension host const LOG_EXTENSION_HOST_COMMUNICATION = false; const LOG_USE_COLORS = true; -const NO_OP_VOID_PROMISE = Promise.resolve(undefined); export class ExtensionHostManager extends Disposable { @@ -38,9 +37,9 @@ export class ExtensionHostManager extends Disposable { public readonly onDidChangeResponsiveState: Event = this._onDidChangeResponsiveState.event; /** - * A map of already activated events to speed things up if the same activation event is triggered multiple times. + * A map of already requested activation events to speed things up if the same activation event is triggered multiple times. */ - private readonly _finishedActivateEvents: { [activationEvent: string]: boolean; }; + private readonly _cachedActivationEvents: Map>; private _rpcProtocol: RPCProtocol | null; private readonly _customers: IDisposable[]; private readonly _extensionHost: IExtensionHost; @@ -57,7 +56,7 @@ export class ExtensionHostManager extends Disposable { @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, ) { super(); - this._finishedActivateEvents = Object.create(null); + this._cachedActivationEvents = new Map>(); this._rpcProtocol = null; this._customers = []; @@ -184,6 +183,7 @@ export class ExtensionHostManager extends Disposable { getProxy: (identifier: ProxyIdentifier): T => this._rpcProtocol!.getProxy(identifier), set: (identifier: ProxyIdentifier, instance: R): R => this._rpcProtocol!.set(identifier, instance), assertRegistered: (identifiers: ProxyIdentifier[]): void => this._rpcProtocol!.assertRegistered(identifiers), + drain: (): Promise => this._rpcProtocol!.drain(), }; // Named customers @@ -218,19 +218,23 @@ export class ExtensionHostManager extends Disposable { } public activateByEvent(activationEvent: string): Promise { - if (this._finishedActivateEvents[activationEvent] || !this._proxy) { - return NO_OP_VOID_PROMISE; + if (!this._cachedActivationEvents.has(activationEvent)) { + this._cachedActivationEvents.set(activationEvent, this._activateByEvent(activationEvent)); } - return this._proxy.then((proxy) => { - if (!proxy) { - // this case is already covered above and logged. - // i.e. the extension host could not be started - return NO_OP_VOID_PROMISE; - } - return proxy.value.$activateByEvent(activationEvent); - }).then(() => { - this._finishedActivateEvents[activationEvent] = true; - }); + return this._cachedActivationEvents.get(activationEvent)!; + } + + private async _activateByEvent(activationEvent: string): Promise { + if (!this._proxy) { + return; + } + const proxy = await this._proxy; + if (!proxy) { + // this case is already covered above and logged. + // i.e. the extension host could not be started + return; + } + return proxy.value.$activateByEvent(activationEvent); } public async getInspectPort(tryEnableInspector: boolean): Promise { diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index 94dbb362146..28b7f1e14b3 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -150,11 +150,13 @@ const extensionKindSchema: IJSONSchema = { type: 'string', enum: [ 'ui', - 'workspace' + 'workspace', + 'web' ], enumDescriptions: [ nls.localize('ui', "UI extension kind. In a remote window, such extensions are enabled only when available on the local machine."), - nls.localize('workspace', "Workspace extension kind. In a remote window, such extensions are enabled only when available on the remote.") + nls.localize('workspace', "Workspace extension kind. In a remote window, such extensions are enabled only when available on the remote."), + nls.localize('web', "Web worker extension kind. Such an extension can execute in a web worker extension host.") ], }; diff --git a/src/vs/workbench/services/extensions/common/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts index 93e7069d659..65e532ee58d 100644 --- a/src/vs/workbench/services/extensions/common/extensionsUtil.ts +++ b/src/vs/workbench/services/extensions/common/extensionsUtil.ts @@ -59,18 +59,29 @@ export function getExtensionKind(manifest: IExtensionManifest, productService: I return toArray(result); } + return deduceExtensionKind(manifest); +} + +export function deduceExtensionKind(manifest: IExtensionManifest): ExtensionKind[] { // Not an UI extension if it has main if (manifest.main) { + if (manifest.browser) { + return ['workspace', 'web']; + } return ['workspace']; } - // Not an UI extension if it has dependencies or an extension pack + if (manifest.browser) { + return ['web']; + } + + // Not an UI nor web extension if it has dependencies or an extension pack if (isNonEmptyArray(manifest.extensionDependencies) || isNonEmptyArray(manifest.extensionPack)) { return ['workspace']; } if (manifest.contributes) { - // Not an UI extension if it has no ui contributions + // Not an UI nor web extension if it has no ui contributions for (const contribution of Object.keys(manifest.contributes)) { if (!isUIExtensionPoint(contribution)) { return ['workspace']; @@ -78,7 +89,7 @@ export function getExtensionKind(manifest: IExtensionManifest, productService: I } } - return ['ui', 'workspace']; + return ['ui', 'workspace', 'web']; } let _uiExtensionPoints: Set | null = null; diff --git a/src/vs/workbench/services/extensions/common/proxyIdentifier.ts b/src/vs/workbench/services/extensions/common/proxyIdentifier.ts index e0e9999a62f..edadcae9eb2 100644 --- a/src/vs/workbench/services/extensions/common/proxyIdentifier.ts +++ b/src/vs/workbench/services/extensions/common/proxyIdentifier.ts @@ -18,6 +18,11 @@ export interface IRPCProtocol { * Assert these identifiers are already registered via `.set`. */ assertRegistered(identifiers: ProxyIdentifier[]): void; + + /** + * Wait for the write buffer (if applicable) to become empty. + */ + drain(): Promise; } export class ProxyIdentifier { diff --git a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts index 5a8bb9b6577..fea56076567 100644 --- a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts +++ b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts @@ -96,7 +96,8 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { } }, signService: this._signService, - logService: this._logService + logService: this._logService, + ipcLogger: null }; return this.remoteAuthorityResolverService.resolveAuthority(this._initDataProvider.remoteAuthority).then((resolverResult) => { @@ -204,8 +205,8 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { const [telemetryInfo, remoteInitData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]); // Collect all identifiers for extension ids which can be considered "resolved" - const resolvedExtensions = remoteInitData.allExtensions.filter(extension => !extension.main).map(extension => extension.identifier); - const hostExtensions = remoteInitData.allExtensions.filter(extension => extension.main && extension.api === 'none').map(extension => extension.identifier); + const resolvedExtensions = remoteInitData.allExtensions.filter(extension => !extension.main && !extension.browser).map(extension => extension.identifier); + const hostExtensions = remoteInitData.allExtensions.filter(extension => (extension.main || extension.browser) && extension.api === 'none').map(extension => extension.identifier); const workspace = this._contextService.getWorkspace(); return { commit: this._productService.commit, diff --git a/src/vs/workbench/services/extensions/common/rpcProtocol.ts b/src/vs/workbench/services/extensions/common/rpcProtocol.ts index 022fd1f4c41..199ea6e15ef 100644 --- a/src/vs/workbench/services/extensions/common/rpcProtocol.ts +++ b/src/vs/workbench/services/extensions/common/rpcProtocol.ts @@ -115,6 +115,13 @@ export class RPCProtocol extends Disposable implements IRPCProtocol { }); } + public drain(): Promise { + if (typeof this._protocol.drain === 'function') { + return this._protocol.drain(); + } + return Promise.resolve(); + } + private _onWillSendRequest(req: number): void { if (this._unacknowledgedCount === 0) { // Since this is the first request we are sending in a while, diff --git a/src/vs/workbench/services/extensions/common/webWorkerIframe.ts b/src/vs/workbench/services/extensions/common/webWorkerIframe.ts new file mode 100644 index 00000000000..bf4731d3909 --- /dev/null +++ b/src/vs/workbench/services/extensions/common/webWorkerIframe.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. + *--------------------------------------------------------------------------------------------*/ + +export const WEB_WORKER_IFRAME = { + sha: 'sha256-rSINb5Ths99Zj4Ml59jEdHS4WbO+H5Iw+oyRmyi2MLw=', + js: ` +(function() { + const workerSrc = document.getElementById('vscode-worker-src').getAttribute('data-value'); + const worker = new Worker(workerSrc, { name: 'WorkerExtensionHost' }); + const vscodeWebWorkerExtHostId = document.getElementById('vscode-web-worker-ext-host-id').getAttribute('data-value'); + + worker.onmessage = (event) => { + const { data } = event; + if (!(data instanceof ArrayBuffer)) { + console.warn('Unknown data received', data); + window.parent.postMessage({ + vscodeWebWorkerExtHostId, + error: { + name: 'Error', + message: 'Unknown data received', + stack: [] + } + }, '*'); + return; + } + window.parent.postMessage({ + vscodeWebWorkerExtHostId, + data: data + }, '*', [data]); + }; + + worker.onerror = (event) => { + console.error(event.message, event.error); + window.parent.postMessage({ + vscodeWebWorkerExtHostId, + error: { + name: event.error ? event.error.name : '', + message: event.error ? event.error.message : '', + stack: event.error ? event.error.stack : [] + } + }, '*'); + }; + + window.addEventListener('message', function(event) { + if (event.source !== window.parent) { + return; + } + if (event.data.vscodeWebWorkerExtHostId !== vscodeWebWorkerExtHostId) { + return; + } + worker.postMessage(event.data.data, [event.data.data]); + }, false); +})(); +` +}; diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index acf4b587af0..512b1c906ed 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -24,7 +24,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IExtensionService, toExtension, ExtensionHostKind, IExtensionHost, webWorkerExtHostConfig } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionHostManager } from 'vs/workbench/services/extensions/common/extensionHostManager'; -import { ExtensionIdentifier, IExtension, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtension, ExtensionType, IExtensionDescription, ExtensionKind } from 'vs/platform/extensions/common/extensions'; import { Schemas } from 'vs/base/common/network'; import { IFileService } from 'vs/platform/files/common/files'; import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection'; @@ -372,7 +372,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten private async _scanAllLocalExtensions(): Promise { return flatten(await Promise.all([ this._extensionScanner.scannedExtensions, - this._webExtensionsScannerService.scanExtensions().then(extensions => extensions.map(parseScannedExtension)) + this._webExtensionsScannerService.scanAndTranslateExtensions().then(extensions => extensions.map(parseScannedExtension)) ])); } @@ -381,7 +381,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten getInitData: async () => { if (isInitialStart) { const localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions()); - const runningLocation = determineRunningLocation(this._productService, this._configurationService, localExtensions, [], false, this._enableLocalWebWorker); + const runningLocation = _determineRunningLocation(this._productService, this._configurationService, localExtensions, [], false, this._enableLocalWebWorker); const localProcessExtensions = filterByRunningLocation(localExtensions, runningLocation, desiredRunningLocation); return { autoStart: false, @@ -498,8 +498,9 @@ export class ExtensionService extends AbstractExtensionService implements IExten const remoteAuthority = this._environmentService.configuration.remoteAuthority; const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess)!; - let localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions()); + const localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions()); let remoteEnv: IRemoteAgentEnvironment | null = null; + let remoteExtensions: IExtensionDescription[] = []; if (remoteAuthority) { let resolverResult: ResolverResult; @@ -538,7 +539,11 @@ export class ExtensionService extends AbstractExtensionService implements IExten } // fetch the remote environment - remoteEnv = await this._remoteAgentService.getEnvironment(); + [remoteEnv, remoteExtensions] = await Promise.all([ + this._remoteAgentService.getEnvironment(), + this._remoteAgentService.scanExtensions() + ]); + remoteExtensions = this._checkEnabledAndProposedAPI(remoteExtensions); if (!remoteEnv) { this._notificationService.notify({ severity: Severity.Error, message: nls.localize('getEnvironmentFailure', "Could not fetch remote environment") }); @@ -548,14 +553,12 @@ export class ExtensionService extends AbstractExtensionService implements IExten } } - await this._startLocalExtensionHost(localExtensions, remoteAuthority, remoteEnv); + await this._startLocalExtensionHost(localExtensions, remoteAuthority, remoteEnv, remoteExtensions); } - private async _startLocalExtensionHost(localExtensions: IExtensionDescription[], remoteAuthority: string | undefined = undefined, remoteEnv: IRemoteAgentEnvironment | null = null): Promise { + private async _startLocalExtensionHost(localExtensions: IExtensionDescription[], remoteAuthority: string | undefined = undefined, remoteEnv: IRemoteAgentEnvironment | null = null, remoteExtensions: IExtensionDescription[] = []): Promise { - let remoteExtensions = remoteEnv ? this._checkEnabledAndProposedAPI(remoteEnv.extensions) : []; - - this._runningLocation = determineRunningLocation(this._productService, this._configurationService, localExtensions, remoteExtensions, Boolean(remoteAuthority), this._enableLocalWebWorker); + this._runningLocation = _determineRunningLocation(this._productService, this._configurationService, localExtensions, remoteExtensions, Boolean(remoteAuthority), this._enableLocalWebWorker); // remove non-UI extensions from the local extensions const localProcessExtensions = filterByRunningLocation(localExtensions, this._runningLocation, ExtensionRunningLocation.LocalProcess); @@ -582,8 +585,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._doHandleExtensionPoints(this._registry.getAllExtensionDescriptions()); - const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess)!; - localProcessExtensionHost.start(localProcessExtensions.map(extension => extension.identifier).filter(id => this._registry.containsExtension(id))); + const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess); + if (localProcessExtensionHost) { + localProcessExtensionHost.start(localProcessExtensions.map(extension => extension.identifier).filter(id => this._registry.containsExtension(id))); + } const localWebWorkerExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalWebWorker); if (localWebWorkerExtensionHost) { @@ -679,7 +684,7 @@ const enum ExtensionRunningLocation { Remote } -function determineRunningLocation(productService: IProductService, configurationService: IConfigurationService, localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[], hasRemote: boolean, hasLocalWebWorker: boolean): Map { +export function determineRunningLocation(localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[], allExtensionKinds: Map, hasRemote: boolean, hasLocalWebWorker: boolean): Map { const localExtensionsSet = new Set(); localExtensions.forEach(ext => localExtensionsSet.add(ExtensionIdentifier.toKey(ext.identifier))); @@ -689,7 +694,8 @@ function determineRunningLocation(productService: IProductService, configuration const pickRunningLocation = (extension: IExtensionDescription): ExtensionRunningLocation => { const isInstalledLocally = localExtensionsSet.has(ExtensionIdentifier.toKey(extension.identifier)); const isInstalledRemotely = remoteExtensionsSet.has(ExtensionIdentifier.toKey(extension.identifier)); - for (const extensionKind of getExtensionKind(extension, productService, configurationService)) { + const extensionKinds = allExtensionKinds.get(ExtensionIdentifier.toKey(extension.identifier)) || []; + for (const extensionKind of extensionKinds) { if (extensionKind === 'ui' && isInstalledLocally) { // ui extensions run locally if possible return ExtensionRunningLocation.LocalProcess; @@ -704,10 +710,6 @@ function determineRunningLocation(productService: IProductService, configuration } if (extensionKind === 'web' && isInstalledLocally && hasLocalWebWorker) { // web worker extensions run in the local web worker if possible - if (typeof extension.browser !== 'undefined') { - // The "browser" field determines the entry point - (extension).main = extension.browser; - } return ExtensionRunningLocation.LocalWebWorker; } } @@ -720,6 +722,13 @@ function determineRunningLocation(productService: IProductService, configuration return runningLocation; } +function _determineRunningLocation(productService: IProductService, configurationService: IConfigurationService, localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[], hasRemote: boolean, hasLocalWebWorker: boolean): Map { + const allExtensionKinds = new Map(); + localExtensions.forEach(ext => allExtensionKinds.set(ExtensionIdentifier.toKey(ext.identifier), getExtensionKind(ext, productService, configurationService))); + remoteExtensions.forEach(ext => allExtensionKinds.set(ExtensionIdentifier.toKey(ext.identifier), getExtensionKind(ext, productService, configurationService))); + return determineRunningLocation(localExtensions, remoteExtensions, allExtensionKinds, hasRemote, hasLocalWebWorker); +} + function filterByRunningLocation(extensions: IExtensionDescription[], runningLocation: Map, desiredRunningLocation: ExtensionRunningLocation): IExtensionDescription[] { return extensions.filter(ext => runningLocation.get(ExtensionIdentifier.toKey(ext.identifier)) === desiredRunningLocation); } diff --git a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts index 05b5fd012d2..49542eda74c 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts @@ -96,10 +96,10 @@ let onTerminate = function () { nativeExit(); }; -function _createExtHostProtocol(): Promise { +function _createExtHostProtocol(): Promise { if (process.env.VSCODE_EXTHOST_WILL_SEND_SOCKET) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { let protocol: PersistentProtocol | null = null; @@ -163,7 +163,7 @@ function _createExtHostProtocol(): Promise { const pipeName = process.env.VSCODE_IPC_HOOK_EXTHOST!; - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const socket = net.createConnection(pipeName, () => { socket.removeListener('error', reject); @@ -203,6 +203,10 @@ async function createExtHostProtocol(): Promise { protocol.send(msg); } } + + drain(): Promise { + return protocol.drain(); + } }; } diff --git a/src/vs/workbench/services/extensions/node/extensionPoints.ts b/src/vs/workbench/services/extensions/node/extensionPoints.ts index ab1480d249b..23795239346 100644 --- a/src/vs/workbench/services/extensions/node/extensionPoints.ts +++ b/src/vs/workbench/services/extensions/node/extensionPoints.ts @@ -389,9 +389,8 @@ class ExtensionManifestValidator extends ExtensionManifestHandler { notices.push(nls.localize('extensionDescription.main1', "property `{0}` can be omitted or must be of type `string`", 'main')); return false; } else { - let normalizedAbsolutePath = path.join(extensionFolderPath, extensionDescription.main); - - if (normalizedAbsolutePath.indexOf(extensionFolderPath)) { + const normalizedAbsolutePath = path.join(extensionFolderPath, extensionDescription.main); + if (!normalizedAbsolutePath.startsWith(extensionFolderPath)) { notices.push(nls.localize('extensionDescription.main2', "Expected `main` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", normalizedAbsolutePath, extensionFolderPath)); // not a failure case } @@ -401,6 +400,22 @@ class ExtensionManifestValidator extends ExtensionManifestHandler { return false; } } + if (typeof extensionDescription.browser !== 'undefined') { + if (typeof extensionDescription.browser !== 'string') { + notices.push(nls.localize('extensionDescription.browser1', "property `{0}` can be omitted or must be of type `string`", 'browser')); + return false; + } else { + const normalizedAbsolutePath = path.join(extensionFolderPath, extensionDescription.browser); + if (!normalizedAbsolutePath.startsWith(extensionFolderPath)) { + notices.push(nls.localize('extensionDescription.browser2', "Expected `browser` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", normalizedAbsolutePath, extensionFolderPath)); + // not a failure case + } + } + if (typeof extensionDescription.activationEvents === 'undefined') { + notices.push(nls.localize('extensionDescription.browser3', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'browser')); + return false; + } + } return true; } diff --git a/src/vs/workbench/services/extensions/test/common/extensionsUtil.test.ts b/src/vs/workbench/services/extensions/test/common/extensionsUtil.test.ts new file mode 100644 index 00000000000..b5c5671dd01 --- /dev/null +++ b/src/vs/workbench/services/extensions/test/common/extensionsUtil.test.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { deduceExtensionKind } from 'vs/workbench/services/extensions/common/extensionsUtil'; +import { IExtensionManifest, ExtensionKind } from 'vs/platform/extensions/common/extensions'; + +suite('ExtensionKind', () => { + + function check(manifest: Partial, expected: ExtensionKind[]): void { + assert.deepEqual(deduceExtensionKind(manifest), expected); + } + + test('declarative with extension dependencies => workspace', () => { + check({ extensionDependencies: ['ext1'] }, ['workspace']); + }); + + test('declarative extension pack => workspace', () => { + check({ extensionPack: ['ext1', 'ext2'] }, ['workspace']); + }); + + test('declarative with unknown contribution point => workspace', () => { + check({ contributes: { 'unknownPoint': { something: true } } }, ['workspace']); + }); + + test('simple declarative => ui, workspace, web', () => { + check({}, ['ui', 'workspace', 'web']); + }); + + test('only browser => web', () => { + check({ browser: 'main.browser.js' }, ['web']); + }); + + test('only main => workspace', () => { + check({ main: 'main.js' }, ['workspace']); + }); + + test('main and browser => workspace, web', () => { + check({ main: 'main.js', browser: 'main.browser.js' }, ['workspace', 'web']); + }); +}); diff --git a/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts b/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts index e2d68b86b98..2041723074a 100644 --- a/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts +++ b/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts @@ -10,6 +10,7 @@ import { isMessageOfType, MessageType, createMessageOfType } from 'vs/workbench/ import { IInitData } from 'vs/workbench/api/common/extHost.protocol'; import { ExtensionHostMain } from 'vs/workbench/services/extensions/common/extensionHostMain'; import { IHostUtils } from 'vs/workbench/api/common/extHostExtensionService'; +import * as path from 'vs/base/common/path'; import 'vs/workbench/api/common/extHost.common.services'; import 'vs/workbench/api/worker/extHost.worker.services'; @@ -35,6 +36,17 @@ self.postMessage = () => console.trace(`'postMessage' has been blocked`); const nativeAddEventLister = addEventListener.bind(self); self.addEventLister = () => console.trace(`'addEventListener' has been blocked`); +if (location.protocol === 'data:') { + // make sure new Worker(...) always uses data: + const _Worker = Worker; + Worker = function (stringUrl: string | URL, options?: WorkerOptions) { + const js = `importScripts('${stringUrl}');`; + options = options || {}; + options.name = options.name || path.basename(stringUrl.toString()); + return new _Worker(`data:text/javascript;charset=utf-8,${encodeURIComponent(js)}`, options); + }; +} + //#endregion --- const hostUtil = new class implements IHostUtils { diff --git a/src/vs/workbench/services/label/common/labelService.ts b/src/vs/workbench/services/label/common/labelService.ts index c3a566e3c3e..66b354a5a71 100644 --- a/src/vs/workbench/services/label/common/labelService.ts +++ b/src/vs/workbench/services/label/common/labelService.ts @@ -51,6 +51,10 @@ const resourceLabelFormattersExtPoint = ExtensionsRegistry.registerExtensionPoin type: 'string', description: localize('vscode.extension.contributes.resourceLabelFormatters.separator', "Separator to be used in the uri label display. '/' or '\' as an example.") }, + stripPathStartingSeparator: { + type: 'boolean', + description: localize('vscode.extension.contributes.resourceLabelFormatters.stripPathStartingSeparator', "Controls whether `${path}` substitutions should have starting separator characters stripped.") + }, tildify: { type: 'boolean', description: localize('vscode.extension.contributes.resourceLabelFormatters.tildify', "Controls if the start of the uri label should be tildified when possible.") @@ -244,7 +248,10 @@ export class LabelService extends Disposable implements ILabelService { switch (token) { case 'scheme': return resource.scheme; case 'authority': return resource.authority; - case 'path': return resource.path; + case 'path': + return formatting.stripPathStartingSeparator + ? resource.path.slice(resource.path[0] === formatting.separator ? 1 : 0) + : resource.path; default: { if (qsToken === 'query') { const { query } = resource; diff --git a/src/vs/workbench/services/label/test/browser/label.test.ts b/src/vs/workbench/services/label/test/browser/label.test.ts index f5d8ff1ab39..8828cd48fd5 100644 --- a/src/vs/workbench/services/label/test/browser/label.test.ts +++ b/src/vs/workbench/services/label/test/browser/label.test.ts @@ -226,6 +226,26 @@ suite('multi-root worksapce', () => { const generated = labelService.getUriLabel(URI.file(path), { relative: true }); assert.equal(generated, label, path); }); + }); + test('stripPathStartingSeparator', () => { + labelService.registerFormatter({ + scheme: 'file', + formatting: { + label: '${path}', + separator: '/', + stripPathStartingSeparator: true + } + }); + + const tests = { + 'folder1/src/file': 'Sources • file', + 'other/blah': 'other/blah', + }; + + Object.entries(tests).forEach(([path, label]) => { + const generated = labelService.getUriLabel(URI.file(path), { relative: true }); + assert.equal(generated, label, path); + }); }); }); diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 3e607042705..d1a79df4f13 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -505,7 +505,9 @@ export class ProgressService extends Disposable implements IProgressService { 'workbench.action.quit', 'workbench.action.reloadWindow', 'copy', - 'cut' + 'cut', + 'editor.action.clipboardCopyAction', + 'editor.action.clipboardCutAction' ]; let dialog: Dialog; diff --git a/src/vs/workbench/services/remote/browser/remoteAgentServiceImpl.ts b/src/vs/workbench/services/remote/browser/remoteAgentServiceImpl.ts index b4202254b39..5a9684c38a1 100644 --- a/src/vs/workbench/services/remote/browser/remoteAgentServiceImpl.ts +++ b/src/vs/workbench/services/remote/browser/remoteAgentServiceImpl.ts @@ -4,21 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { AbstractRemoteAgentService, RemoteAgentConnection } from 'vs/workbench/services/remote/common/abstractRemoteAgentService'; +import { AbstractRemoteAgentService } from 'vs/workbench/services/remote/common/abstractRemoteAgentService'; import { IProductService } from 'vs/platform/product/common/productService'; import { IWebSocketFactory, BrowserSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory'; import { ISignService } from 'vs/platform/sign/common/sign'; -import { ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; import { ILogService } from 'vs/platform/log/common/log'; export class RemoteAgentService extends AbstractRemoteAgentService implements IRemoteAgentService { - public readonly socketFactory: ISocketFactory; - - private readonly _connection: IRemoteAgentConnection | null = null; - constructor( webSocketFactory: IWebSocketFactory | null | undefined, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @@ -27,16 +22,6 @@ export class RemoteAgentService extends AbstractRemoteAgentService implements IR @ISignService signService: ISignService, @ILogService logService: ILogService ) { - super(environmentService, remoteAuthorityResolverService); - - this.socketFactory = new BrowserSocketFactory(webSocketFactory); - const remoteAuthority = environmentService.configuration.remoteAuthority; - if (remoteAuthority) { - this._connection = this._register(new RemoteAgentConnection(remoteAuthority, productService.commit, this.socketFactory, remoteAuthorityResolverService, signService, logService)); - } - } - - getConnection(): IRemoteAgentConnection | null { - return this._connection; + super(new BrowserSocketFactory(webSocketFactory), environmentService, productService, remoteAuthorityResolverService, signService, logService); } } diff --git a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts index 974d3ea1bbb..61b727c08fa 100644 --- a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts @@ -5,9 +5,9 @@ import * as nls from 'vs/nls'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IChannel, IServerChannel, getDelayedChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IChannel, IServerChannel, getDelayedChannel, IPCLogger } from 'vs/base/parts/ipc/common/ipc'; import { Client } from 'vs/base/parts/ipc/common/ipc.net'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { connectRemoteAgentManagement, IConnectionOptions, ISocketFactory, PersistenConnectionEvent } from 'vs/platform/remote/common/remoteAgentConnection'; import { IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver'; @@ -22,35 +22,62 @@ import { Emitter } from 'vs/base/common/event'; import { ISignService } from 'vs/platform/sign/common/sign'; import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IProductService } from 'vs/platform/product/common/productService'; -export abstract class AbstractRemoteAgentService extends Disposable { +export abstract class AbstractRemoteAgentService extends Disposable implements IRemoteAgentService { declare readonly _serviceBrand: undefined; + public readonly socketFactory: ISocketFactory; + private readonly _connection: IRemoteAgentConnection | null; private _environment: Promise | null; constructor( - @IEnvironmentService protected readonly _environmentService: IEnvironmentService, - @IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService + socketFactory: ISocketFactory, + @IWorkbenchEnvironmentService protected readonly _environmentService: IWorkbenchEnvironmentService, + @IProductService productService: IProductService, + @IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, + @ISignService signService: ISignService, + @ILogService logService: ILogService ) { super(); + this.socketFactory = socketFactory; + if (this._environmentService.configuration.remoteAuthority) { + this._connection = this._register(new RemoteAgentConnection(this._environmentService.configuration.remoteAuthority, productService.commit, this.socketFactory, this._remoteAuthorityResolverService, signService, logService)); + } else { + this._connection = null; + } this._environment = null; } - abstract getConnection(): IRemoteAgentConnection | null; + getConnection(): IRemoteAgentConnection | null { + return this._connection; + } - getEnvironment(bail?: boolean): Promise { + getEnvironment(): Promise { + return this.getRawEnvironment().then(undefined, () => null); + } + + getRawEnvironment(): Promise { if (!this._environment) { this._environment = this._withChannel( async (channel, connection) => { - const env = await RemoteExtensionEnvironmentChannelClient.getEnvironmentData(channel, connection.remoteAuthority, this._environmentService.extensionDevelopmentLocationURI); + const env = await RemoteExtensionEnvironmentChannelClient.getEnvironmentData(channel, connection.remoteAuthority); this._remoteAuthorityResolverService._setAuthorityConnectionToken(connection.remoteAuthority, env.connectionToken); return env; }, null ); } - return bail ? this._environment : this._environment.then(undefined, () => null); + return this._environment; + } + + scanExtensions(skipExtensions: ExtensionIdentifier[] = []): Promise { + return this._withChannel( + (channel, connection) => RemoteExtensionEnvironmentChannelClient.scanExtensions(channel, connection.remoteAuthority, this._environmentService.extensionDevelopmentLocationURI, skipExtensions), + [] + ).then(undefined, () => []); } getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise { @@ -152,7 +179,8 @@ export class RemoteAgentConnection extends Disposable implements IRemoteAgentCon } }, signService: this._signService, - logService: this._logService + logService: this._logService, + ipcLogger: false ? new IPCLogger(`Local \u2192 Remote`, `Remote \u2192 Local`) : null }; const connection = this._register(await connectRemoteAgentManagement(options, this.remoteAuthority, `renderer`)); this._register(connection.onDidStateChange(e => this._onDidStateChange.fire(e))); @@ -167,7 +195,7 @@ class RemoteConnectionFailureNotificationContribution implements IWorkbenchContr @INotificationService notificationService: INotificationService, ) { // Let's cover the case where connecting to fetch the remote extension info fails - remoteAgentService.getEnvironment(true) + remoteAgentService.getRawEnvironment() .then(undefined, err => { if (!RemoteAuthorityResolverError.isHandled(err)) { notificationService.error(nls.localize('connectionError', "Failed to connect to the remote extension host server (Error: {0})", err ? err.message : '')); diff --git a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts index f37e2c744eb..052cd072d93 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts @@ -6,15 +6,20 @@ import * as platform from 'vs/base/common/platform'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription, ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; export interface IGetEnvironmentDataArguments { + remoteAuthority: string; +} + +export interface IScanExtensionsArguments { language: string; remoteAuthority: string; extensionDevelopmentPath: UriComponents[] | undefined; + skipExtensions: ExtensionIdentifier[]; } export interface IRemoteAgentEnvironmentDTO { @@ -28,17 +33,14 @@ export interface IRemoteAgentEnvironmentDTO { globalStorageHome: UriComponents; workspaceStorageHome: UriComponents; userHome: UriComponents; - extensions: IExtensionDescription[]; os: platform.OperatingSystem; } export class RemoteExtensionEnvironmentChannelClient { - static async getEnvironmentData(channel: IChannel, remoteAuthority: string, extensionDevelopmentPath?: URI[]): Promise { + static async getEnvironmentData(channel: IChannel, remoteAuthority: string): Promise { const args: IGetEnvironmentDataArguments = { - language: platform.language, - remoteAuthority, - extensionDevelopmentPath + remoteAuthority }; const data = await channel.call('getEnvironmentData', args); @@ -54,11 +56,24 @@ export class RemoteExtensionEnvironmentChannelClient { globalStorageHome: URI.revive(data.globalStorageHome), workspaceStorageHome: URI.revive(data.workspaceStorageHome), userHome: URI.revive(data.userHome), - extensions: data.extensions.map(ext => { (ext).extensionLocation = URI.revive(ext.extensionLocation); return ext; }), os: data.os }; } + static async scanExtensions(channel: IChannel, remoteAuthority: string, extensionDevelopmentPath: URI[] | undefined, skipExtensions: ExtensionIdentifier[]): Promise { + const args: IScanExtensionsArguments = { + language: platform.language, + remoteAuthority, + extensionDevelopmentPath, + skipExtensions + }; + + const extensions = await channel.call('scanExtensions', args); + extensions.forEach(ext => { (ext).extensionLocation = URI.revive(ext.extensionLocation); }); + + return extensions; + } + static getDiagnosticInfo(channel: IChannel, options: IDiagnosticInfoOptions): Promise { return channel.call('getDiagnosticInfo', options); } diff --git a/src/vs/workbench/services/remote/common/remoteAgentService.ts b/src/vs/workbench/services/remote/common/remoteAgentService.ts index eeb1cc69c67..2b1a17c2aca 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentService.ts @@ -10,6 +10,7 @@ import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics import { Event } from 'vs/base/common/event'; import { PersistenConnectionEvent as PersistentConnectionEvent, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; export const RemoteExtensionLogFileName = 'remoteagent'; @@ -21,7 +22,18 @@ export interface IRemoteAgentService { readonly socketFactory: ISocketFactory; getConnection(): IRemoteAgentConnection | null; - getEnvironment(bail?: boolean): Promise; + /** + * Get the remote environment. In case of an error, returns `null`. + */ + getEnvironment(): Promise; + /** + * Get the remote environment. Can return an error. + */ + getRawEnvironment(): Promise; + /** + * Scan remote extensions. + */ + scanExtensions(skipExtensions?: ExtensionIdentifier[]): Promise; getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise; disableTelemetry(): Promise; logTelemetry(eventName: string, data?: ITelemetryData): Promise; diff --git a/src/vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl.ts b/src/vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl.ts index 748bb47d1a7..a0cc5572f4e 100644 --- a/src/vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl.ts +++ b/src/vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl.ts @@ -3,37 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IProductService } from 'vs/platform/product/common/productService'; import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory'; -import { AbstractRemoteAgentService, RemoteAgentConnection } from 'vs/workbench/services/remote/common/abstractRemoteAgentService'; +import { AbstractRemoteAgentService } from 'vs/workbench/services/remote/common/abstractRemoteAgentService'; import { ISignService } from 'vs/platform/sign/common/sign'; -import { ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export class RemoteAgentService extends AbstractRemoteAgentService implements IRemoteAgentService { - - public readonly socketFactory: ISocketFactory; - - private readonly _connection: IRemoteAgentConnection | null = null; - constructor( @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IProductService productService: IProductService, @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, @ISignService signService: ISignService, @ILogService logService: ILogService, - @IProductService productService: IProductService ) { - super(environmentService, remoteAuthorityResolverService); - this.socketFactory = nodeSocketFactory; - if (environmentService.configuration.remoteAuthority) { - this._connection = this._register(new RemoteAgentConnection(environmentService.configuration.remoteAuthority, productService.commit, nodeSocketFactory, remoteAuthorityResolverService, signService, logService)); - } - } - - getConnection(): IRemoteAgentConnection | null { - return this._connection; + super(nodeSocketFactory, environmentService, productService, remoteAuthorityResolverService, signService, logService); } } diff --git a/src/vs/workbench/services/search/common/replace.ts b/src/vs/workbench/services/search/common/replace.ts index 2bbbf1ffa58..9dd7567231c 100644 --- a/src/vs/workbench/services/search/common/replace.ts +++ b/src/vs/workbench/services/search/common/replace.ts @@ -57,13 +57,13 @@ export class ReplacePattern { */ getReplaceString(text: string, preserveCase?: boolean): string | null { this._regExp.lastIndex = 0; - let match = this._regExp.exec(text); + const match = this._regExp.exec(text); if (match) { if (this.hasParameters) { if (match[0] === text) { return text.replace(this._regExp, this.buildReplaceString(match, preserveCase)); } - let replaceString = text.replace(this._regExp, this.buildReplaceString(match, preserveCase)); + 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); @@ -94,7 +94,7 @@ export class ReplacePattern { let substrFrom = 0, result = ''; for (let i = 0, len = replaceString.length; i < len; i++) { - let chCode = replaceString.charCodeAt(i); + const chCode = replaceString.charCodeAt(i); if (chCode === CharCode.Backslash) { @@ -106,7 +106,7 @@ export class ReplacePattern { break; } - let nextChCode = replaceString.charCodeAt(i); + const nextChCode = replaceString.charCodeAt(i); let replaceWithCharacter: string | null = null; switch (nextChCode) { @@ -140,7 +140,7 @@ export class ReplacePattern { break; } - let nextChCode = replaceString.charCodeAt(i); + const nextChCode = replaceString.charCodeAt(i); let replaceWithCharacter: string | null = null; switch (nextChCode) { diff --git a/src/vs/workbench/services/search/common/searchService.ts b/src/vs/workbench/services/search/common/searchService.ts index 196da4b7a1f..cb047264d05 100644 --- a/src/vs/workbench/services/search/common/searchService.ts +++ b/src/vs/workbench/services/search/common/searchService.ts @@ -179,7 +179,7 @@ export class SearchService extends Disposable implements ISearchService { } private async waitForProvider(queryType: QueryType, scheme: string): Promise { - let deferredMap: Map> = queryType === QueryType.File ? + const deferredMap: Map> = queryType === QueryType.File ? this.deferredFileSearchesByScheme : this.deferredTextSearchesByScheme; diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index ec87a8453da..2042e69d335 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -205,7 +205,7 @@ export class FileWalker { .map(arg => arg.match(/^-/) ? arg : `'${arg}'`) .join(' '); - let rgCmd = `rg ${escapedArgs}\n - cwd: ${ripgrep.cwd}`; + let rgCmd = `${ripgrep.rgDiskPath} ${escapedArgs}\n - cwd: ${ripgrep.cwd}`; if (ripgrep.rgArgs.siblingClauses) { rgCmd += `\n - Sibling clauses: ${JSON.stringify(ripgrep.rgArgs.siblingClauses)}`; } diff --git a/src/vs/workbench/services/search/node/ripgrepFileSearch.ts b/src/vs/workbench/services/search/node/ripgrepFileSearch.ts index 7e072a9efd8..fdf210f00e6 100644 --- a/src/vs/workbench/services/search/node/ripgrepFileSearch.ts +++ b/src/vs/workbench/services/search/node/ripgrepFileSearch.ts @@ -23,6 +23,7 @@ export function spawnRipgrepCmd(config: IFileQuery, folderQuery: IFolderQuery, i const cwd = folderQuery.folder.fsPath; return { cmd: cp.spawn(rgDiskPath, rgArgs.args, { cwd }), + rgDiskPath, siblingClauses: rgArgs.siblingClauses, rgArgs, cwd diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts index 7db49ba8ef9..006b8fec0df 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts @@ -44,7 +44,7 @@ export class RipgrepTextSearchEngine { const escapedArgs = rgArgs .map(arg => arg.match(/^-/) ? arg : `'${arg}'`) .join(' '); - this.outputChannel.appendLine(`rg ${escapedArgs}\n - cwd: ${cwd}`); + this.outputChannel.appendLine(`${rgDiskPath} ${escapedArgs}\n - cwd: ${cwd}`); let rgProc: Maybe = cp.spawn(rgDiskPath, rgArgs, { cwd }); rgProc.on('error', e => { @@ -57,6 +57,7 @@ export class RipgrepTextSearchEngine { const ripgrepParser = new RipgrepParser(options.maxResults, cwd, options.previewOptions); ripgrepParser.on('result', (match: TextSearchResult) => { gotResult = true; + dataWithoutResult = ''; progress.report(match); }); @@ -79,8 +80,12 @@ export class RipgrepTextSearchEngine { cancel(); }); + let dataWithoutResult = ''; rgProc.stdout!.on('data', data => { ripgrepParser.handleData(data); + if (!gotResult) { + dataWithoutResult += data; + } }); let gotData = false; @@ -96,7 +101,12 @@ export class RipgrepTextSearchEngine { rgProc.on('close', () => { this.outputChannel.appendLine(gotData ? 'Got data from stdout' : 'No data from stdout'); this.outputChannel.appendLine(gotResult ? 'Got result from parser' : 'No result from parser'); + if (dataWithoutResult) { + this.outputChannel.appendLine(`Got data without result: ${dataWithoutResult}`); + } + this.outputChannel.appendLine(''); + if (isDone) { resolve({ limitHit }); } else { @@ -152,12 +162,12 @@ export function rgErrorMsgForDisplay(msg: string): Maybe { } export function buildRegexParseError(lines: string[]): string { - let errorMessage: string[] = ['Regex parse error']; - let pcre2ErrorLine = lines.filter(l => (l.startsWith('PCRE2:'))); + const errorMessage: string[] = ['Regex parse error']; + const pcre2ErrorLine = lines.filter(l => (l.startsWith('PCRE2:'))); if (pcre2ErrorLine.length >= 1) { - let pcre2ErrorMessage = pcre2ErrorLine[0].replace('PCRE2:', ''); + const pcre2ErrorMessage = pcre2ErrorLine[0].replace('PCRE2:', ''); if (pcre2ErrorMessage.indexOf(':') !== -1 && pcre2ErrorMessage.split(':').length >= 2) { - let pcre2ActualErrorMessage = pcre2ErrorMessage.split(':')[1]; + const pcre2ActualErrorMessage = pcre2ErrorMessage.split(':')[1]; errorMessage.push(':' + pcre2ActualErrorMessage); } } @@ -290,12 +300,12 @@ export class RipgrepParser extends EventEmitter { match.end = match.end <= 3 ? 0 : match.end - 3; } const inBetweenChars = fullTextBytes.slice(prevMatchEnd, match.start).toString().length; - let startCol = prevMatchEndCol + inBetweenChars; + const startCol = prevMatchEndCol + inBetweenChars; const stats = getNumLinesAndLastNewlineLength(matchText); const startLineNumber = prevMatchEndLine; const endLineNumber = stats.numLines + startLineNumber; - let endCol = stats.numLines > 0 ? + const endCol = stats.numLines > 0 ? stats.lastLineLength : stats.lastLineLength + startCol; 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 7672cec39d5..7180e0fb43e 100644 --- a/src/vs/workbench/services/search/test/common/replace.test.ts +++ b/src/vs/workbench/services/search/test/common/replace.test.ts @@ -8,7 +8,7 @@ import { ReplacePattern } from 'vs/workbench/services/search/common/replace'; suite('Replace Pattern test', () => { test('parse replace string', () => { - let testParse = (input: string, expected: string, expectedHasParameters: boolean) => { + const testParse = (input: string, expected: string, expectedHasParameters: boolean) => { let actual = new ReplacePattern(input, { pattern: 'somepattern', isRegExp: true }); assert.equal(expected, actual.pattern); assert.equal(expectedHasParameters, actual.hasParameters); diff --git a/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts b/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts index 7eb76ace39d..f2ad6f9a9e2 100644 --- a/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts +++ b/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts @@ -387,7 +387,7 @@ suite('TextSearch-integration', function () { throw new Error('expected fail'); }, err => { const searchError = deserializeSearchError(err); - let regexParseErrorForUnclosedParenthesis = 'Regex parse error: unmatched closing parenthesis'; + const regexParseErrorForUnclosedParenthesis = 'Regex parse error: unmatched closing parenthesis'; assert.equal(searchError.message, regexParseErrorForUnclosedParenthesis); assert.equal(searchError.code, SearchErrorCode.regexParseError); }); @@ -404,7 +404,7 @@ suite('TextSearch-integration', function () { throw new Error('expected fail'); }, err => { const searchError = deserializeSearchError(err); - let regexParseErrorForLookAround = 'Regex parse error: lookbehind assertion is not fixed length'; + const regexParseErrorForLookAround = 'Regex parse error: lookbehind assertion is not fixed length'; assert.equal(searchError.message, regexParseErrorForLookAround); assert.equal(searchError.code, SearchErrorCode.regexParseError); }); diff --git a/src/vs/workbench/services/telemetry/browser/telemetryService.ts b/src/vs/workbench/services/telemetry/browser/telemetryService.ts index dd6ed7685bf..41b39345f1e 100644 --- a/src/vs/workbench/services/telemetry/browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/browser/telemetryService.ts @@ -65,6 +65,10 @@ export class TelemetryService extends Disposable implements ITelemetryService { return this.impl.setEnabled(value); } + setExperimentProperty(name: string, value: string): void { + return this.impl.setExperimentProperty(name, value); + } + get isOptedIn(): boolean { return this.impl.isOptedIn; } diff --git a/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts b/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts index 2b32c4425df..99d36e3fa46 100644 --- a/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts @@ -57,6 +57,10 @@ export class TelemetryService extends Disposable implements ITelemetryService { return this.impl.setEnabled(value); } + setExperimentProperty(name: string, value: string): void { + return this.impl.setExperimentProperty(name, value); + } + get isOptedIn(): boolean { return this.impl.isOptedIn; } diff --git a/src/vs/workbench/services/textfile/common/encoding.ts b/src/vs/workbench/services/textfile/common/encoding.ts index 7c390e47caa..c4f80a86739 100644 --- a/src/vs/workbench/services/textfile/common/encoding.ts +++ b/src/vs/workbench/services/textfile/common/encoding.ts @@ -446,3 +446,244 @@ export function detectEncodingFromBuffer({ buffer, bytesRead }: IReadResult, aut return { seemsBinary, encoding }; } + +export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; labelShort: string; order: number; encodeOnly?: boolean; alias?: string } } = { + utf8: { + labelLong: 'UTF-8', + labelShort: 'UTF-8', + order: 1, + alias: 'utf8bom' + }, + utf8bom: { + labelLong: 'UTF-8 with BOM', + labelShort: 'UTF-8 with BOM', + encodeOnly: true, + order: 2, + alias: 'utf8' + }, + utf16le: { + labelLong: 'UTF-16 LE', + labelShort: 'UTF-16 LE', + order: 3 + }, + utf16be: { + labelLong: 'UTF-16 BE', + labelShort: 'UTF-16 BE', + order: 4 + }, + windows1252: { + labelLong: 'Western (Windows 1252)', + labelShort: 'Windows 1252', + order: 5 + }, + iso88591: { + labelLong: 'Western (ISO 8859-1)', + labelShort: 'ISO 8859-1', + order: 6 + }, + iso88593: { + labelLong: 'Western (ISO 8859-3)', + labelShort: 'ISO 8859-3', + order: 7 + }, + iso885915: { + labelLong: 'Western (ISO 8859-15)', + labelShort: 'ISO 8859-15', + order: 8 + }, + macroman: { + labelLong: 'Western (Mac Roman)', + labelShort: 'Mac Roman', + order: 9 + }, + cp437: { + labelLong: 'DOS (CP 437)', + labelShort: 'CP437', + order: 10 + }, + windows1256: { + labelLong: 'Arabic (Windows 1256)', + labelShort: 'Windows 1256', + order: 11 + }, + iso88596: { + labelLong: 'Arabic (ISO 8859-6)', + labelShort: 'ISO 8859-6', + order: 12 + }, + windows1257: { + labelLong: 'Baltic (Windows 1257)', + labelShort: 'Windows 1257', + order: 13 + }, + iso88594: { + labelLong: 'Baltic (ISO 8859-4)', + labelShort: 'ISO 8859-4', + order: 14 + }, + iso885914: { + labelLong: 'Celtic (ISO 8859-14)', + labelShort: 'ISO 8859-14', + order: 15 + }, + windows1250: { + labelLong: 'Central European (Windows 1250)', + labelShort: 'Windows 1250', + order: 16 + }, + iso88592: { + labelLong: 'Central European (ISO 8859-2)', + labelShort: 'ISO 8859-2', + order: 17 + }, + cp852: { + labelLong: 'Central European (CP 852)', + labelShort: 'CP 852', + order: 18 + }, + windows1251: { + labelLong: 'Cyrillic (Windows 1251)', + labelShort: 'Windows 1251', + order: 19 + }, + cp866: { + labelLong: 'Cyrillic (CP 866)', + labelShort: 'CP 866', + order: 20 + }, + iso88595: { + labelLong: 'Cyrillic (ISO 8859-5)', + labelShort: 'ISO 8859-5', + order: 21 + }, + koi8r: { + labelLong: 'Cyrillic (KOI8-R)', + labelShort: 'KOI8-R', + order: 22 + }, + koi8u: { + labelLong: 'Cyrillic (KOI8-U)', + labelShort: 'KOI8-U', + order: 23 + }, + iso885913: { + labelLong: 'Estonian (ISO 8859-13)', + labelShort: 'ISO 8859-13', + order: 24 + }, + windows1253: { + labelLong: 'Greek (Windows 1253)', + labelShort: 'Windows 1253', + order: 25 + }, + iso88597: { + labelLong: 'Greek (ISO 8859-7)', + labelShort: 'ISO 8859-7', + order: 26 + }, + windows1255: { + labelLong: 'Hebrew (Windows 1255)', + labelShort: 'Windows 1255', + order: 27 + }, + iso88598: { + labelLong: 'Hebrew (ISO 8859-8)', + labelShort: 'ISO 8859-8', + order: 28 + }, + iso885910: { + labelLong: 'Nordic (ISO 8859-10)', + labelShort: 'ISO 8859-10', + order: 29 + }, + iso885916: { + labelLong: 'Romanian (ISO 8859-16)', + labelShort: 'ISO 8859-16', + order: 30 + }, + windows1254: { + labelLong: 'Turkish (Windows 1254)', + labelShort: 'Windows 1254', + order: 31 + }, + iso88599: { + labelLong: 'Turkish (ISO 8859-9)', + labelShort: 'ISO 8859-9', + order: 32 + }, + windows1258: { + labelLong: 'Vietnamese (Windows 1258)', + labelShort: 'Windows 1258', + order: 33 + }, + gbk: { + labelLong: 'Simplified Chinese (GBK)', + labelShort: 'GBK', + order: 34 + }, + gb18030: { + labelLong: 'Simplified Chinese (GB18030)', + labelShort: 'GB18030', + order: 35 + }, + cp950: { + labelLong: 'Traditional Chinese (Big5)', + labelShort: 'Big5', + order: 36 + }, + big5hkscs: { + labelLong: 'Traditional Chinese (Big5-HKSCS)', + labelShort: 'Big5-HKSCS', + order: 37 + }, + shiftjis: { + labelLong: 'Japanese (Shift JIS)', + labelShort: 'Shift JIS', + order: 38 + }, + eucjp: { + labelLong: 'Japanese (EUC-JP)', + labelShort: 'EUC-JP', + order: 39 + }, + euckr: { + labelLong: 'Korean (EUC-KR)', + labelShort: 'EUC-KR', + order: 40 + }, + windows874: { + labelLong: 'Thai (Windows 874)', + labelShort: 'Windows 874', + order: 41 + }, + iso885911: { + labelLong: 'Latin/Thai (ISO 8859-11)', + labelShort: 'ISO 8859-11', + order: 42 + }, + koi8ru: { + labelLong: 'Cyrillic (KOI8-RU)', + labelShort: 'KOI8-RU', + order: 43 + }, + koi8t: { + labelLong: 'Tajik (KOI8-T)', + labelShort: 'KOI8-T', + order: 44 + }, + gb2312: { + labelLong: 'Simplified Chinese (GB 2312)', + labelShort: 'GB 2312', + order: 45 + }, + cp865: { + labelLong: 'Nordic DOS (CP 865)', + labelShort: 'CP 865', + order: 46 + }, + cp850: { + labelLong: 'Western European DOS (CP 850)', + labelShort: 'CP 850', + order: 47 + } +}; diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 6310f919b72..d974bea6d5e 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -506,244 +506,3 @@ export function toBufferOrReadable(value: string | ITextSnapshot | undefined): V return new TextSnapshotReadable(value); } - -export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; labelShort: string; order: number; encodeOnly?: boolean; alias?: string } } = { - utf8: { - labelLong: 'UTF-8', - labelShort: 'UTF-8', - order: 1, - alias: 'utf8bom' - }, - utf8bom: { - labelLong: 'UTF-8 with BOM', - labelShort: 'UTF-8 with BOM', - encodeOnly: true, - order: 2, - alias: 'utf8' - }, - utf16le: { - labelLong: 'UTF-16 LE', - labelShort: 'UTF-16 LE', - order: 3 - }, - utf16be: { - labelLong: 'UTF-16 BE', - labelShort: 'UTF-16 BE', - order: 4 - }, - windows1252: { - labelLong: 'Western (Windows 1252)', - labelShort: 'Windows 1252', - order: 5 - }, - iso88591: { - labelLong: 'Western (ISO 8859-1)', - labelShort: 'ISO 8859-1', - order: 6 - }, - iso88593: { - labelLong: 'Western (ISO 8859-3)', - labelShort: 'ISO 8859-3', - order: 7 - }, - iso885915: { - labelLong: 'Western (ISO 8859-15)', - labelShort: 'ISO 8859-15', - order: 8 - }, - macroman: { - labelLong: 'Western (Mac Roman)', - labelShort: 'Mac Roman', - order: 9 - }, - cp437: { - labelLong: 'DOS (CP 437)', - labelShort: 'CP437', - order: 10 - }, - windows1256: { - labelLong: 'Arabic (Windows 1256)', - labelShort: 'Windows 1256', - order: 11 - }, - iso88596: { - labelLong: 'Arabic (ISO 8859-6)', - labelShort: 'ISO 8859-6', - order: 12 - }, - windows1257: { - labelLong: 'Baltic (Windows 1257)', - labelShort: 'Windows 1257', - order: 13 - }, - iso88594: { - labelLong: 'Baltic (ISO 8859-4)', - labelShort: 'ISO 8859-4', - order: 14 - }, - iso885914: { - labelLong: 'Celtic (ISO 8859-14)', - labelShort: 'ISO 8859-14', - order: 15 - }, - windows1250: { - labelLong: 'Central European (Windows 1250)', - labelShort: 'Windows 1250', - order: 16 - }, - iso88592: { - labelLong: 'Central European (ISO 8859-2)', - labelShort: 'ISO 8859-2', - order: 17 - }, - cp852: { - labelLong: 'Central European (CP 852)', - labelShort: 'CP 852', - order: 18 - }, - windows1251: { - labelLong: 'Cyrillic (Windows 1251)', - labelShort: 'Windows 1251', - order: 19 - }, - cp866: { - labelLong: 'Cyrillic (CP 866)', - labelShort: 'CP 866', - order: 20 - }, - iso88595: { - labelLong: 'Cyrillic (ISO 8859-5)', - labelShort: 'ISO 8859-5', - order: 21 - }, - koi8r: { - labelLong: 'Cyrillic (KOI8-R)', - labelShort: 'KOI8-R', - order: 22 - }, - koi8u: { - labelLong: 'Cyrillic (KOI8-U)', - labelShort: 'KOI8-U', - order: 23 - }, - iso885913: { - labelLong: 'Estonian (ISO 8859-13)', - labelShort: 'ISO 8859-13', - order: 24 - }, - windows1253: { - labelLong: 'Greek (Windows 1253)', - labelShort: 'Windows 1253', - order: 25 - }, - iso88597: { - labelLong: 'Greek (ISO 8859-7)', - labelShort: 'ISO 8859-7', - order: 26 - }, - windows1255: { - labelLong: 'Hebrew (Windows 1255)', - labelShort: 'Windows 1255', - order: 27 - }, - iso88598: { - labelLong: 'Hebrew (ISO 8859-8)', - labelShort: 'ISO 8859-8', - order: 28 - }, - iso885910: { - labelLong: 'Nordic (ISO 8859-10)', - labelShort: 'ISO 8859-10', - order: 29 - }, - iso885916: { - labelLong: 'Romanian (ISO 8859-16)', - labelShort: 'ISO 8859-16', - order: 30 - }, - windows1254: { - labelLong: 'Turkish (Windows 1254)', - labelShort: 'Windows 1254', - order: 31 - }, - iso88599: { - labelLong: 'Turkish (ISO 8859-9)', - labelShort: 'ISO 8859-9', - order: 32 - }, - windows1258: { - labelLong: 'Vietnamese (Windows 1258)', - labelShort: 'Windows 1258', - order: 33 - }, - gbk: { - labelLong: 'Simplified Chinese (GBK)', - labelShort: 'GBK', - order: 34 - }, - gb18030: { - labelLong: 'Simplified Chinese (GB18030)', - labelShort: 'GB18030', - order: 35 - }, - cp950: { - labelLong: 'Traditional Chinese (Big5)', - labelShort: 'Big5', - order: 36 - }, - big5hkscs: { - labelLong: 'Traditional Chinese (Big5-HKSCS)', - labelShort: 'Big5-HKSCS', - order: 37 - }, - shiftjis: { - labelLong: 'Japanese (Shift JIS)', - labelShort: 'Shift JIS', - order: 38 - }, - eucjp: { - labelLong: 'Japanese (EUC-JP)', - labelShort: 'EUC-JP', - order: 39 - }, - euckr: { - labelLong: 'Korean (EUC-KR)', - labelShort: 'EUC-KR', - order: 40 - }, - windows874: { - labelLong: 'Thai (Windows 874)', - labelShort: 'Windows 874', - order: 41 - }, - iso885911: { - labelLong: 'Latin/Thai (ISO 8859-11)', - labelShort: 'ISO 8859-11', - order: 42 - }, - koi8ru: { - labelLong: 'Cyrillic (KOI8-RU)', - labelShort: 'KOI8-RU', - order: 43 - }, - koi8t: { - labelLong: 'Tajik (KOI8-T)', - labelShort: 'KOI8-T', - order: 44 - }, - gb2312: { - labelLong: 'Simplified Chinese (GB 2312)', - labelShort: 'GB 2312', - order: 45 - }, - cp865: { - labelLong: 'Nordic DOS (CP 865)', - labelShort: 'CP 865', - order: 46 - }, - cp850: { - labelLong: 'Western European DOS (CP 850)', - labelShort: 'CP 850', - order: 47 - } -}; diff --git a/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts b/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts index f110a6b49f7..37930430364 100644 --- a/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts +++ b/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts @@ -11,7 +11,6 @@ import * as streams from 'vs/base/common/stream'; import * as iconv from 'iconv-lite-umd'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { newWriteableBufferStream, VSBuffer, VSBufferReadableStream, streamToBufferReadableStream } from 'vs/base/common/buffer'; -import { SUPPORTED_ENCODINGS } from 'vs/workbench/services/textfile/common/textfiles'; import { isWindows } from 'vs/base/common/platform'; export async function detectEncodingByBOM(file: string): Promise { @@ -427,7 +426,7 @@ suite('Encoding', () => { }); test('encodingExists', async function () { - for (const enc in SUPPORTED_ENCODINGS) { + for (const enc in encoding.SUPPORTED_ENCODINGS) { if (enc === encoding.UTF8_with_bom) { continue; // skip over encodings from us } diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 820fc5ca881..bdac6f7a6c6 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -3,21 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, IAuthenticationProvider, getUserDataSyncStore, isAuthenticationProvider, IUserDataAutoSyncService, SyncResource, IResourcePreview, ISyncResourcePreview, Change, IManualSyncTask } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IAuthenticationProvider, isAuthenticationProvider, IUserDataAutoSyncService, SyncResource, IResourcePreview, ISyncResourcePreview, Change, IManualSyncTask, IUserDataSyncStoreManagementService, UserDataSyncStoreType, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IUserDataSyncWorkbenchService, IUserDataSyncAccount, AccountStatus, CONTEXT_SYNC_ENABLEMENT, CONTEXT_SYNC_STATE, CONTEXT_ACCOUNT_STATE, SHOW_SYNC_LOG_COMMAND_ID, getSyncAreaLabel, IUserDataSyncPreview, IUserDataSyncResource, CONTEXT_ENABLE_MANUAL_SYNC_VIEW, MANUAL_SYNC_VIEW_ID, CONTEXT_ENABLE_ACTIVITY_VIEWS, SYNC_VIEW_CONTAINER_ID } from 'vs/workbench/services/userDataSync/common/userDataSync'; +import { IUserDataSyncWorkbenchService, IUserDataSyncAccount, AccountStatus, CONTEXT_SYNC_ENABLEMENT, CONTEXT_SYNC_STATE, CONTEXT_ACCOUNT_STATE, SHOW_SYNC_LOG_COMMAND_ID, getSyncAreaLabel, IUserDataSyncPreview, IUserDataSyncResource, CONTEXT_ENABLE_SYNC_MERGES_VIEW, SYNC_MERGES_VIEW_ID, CONTEXT_ENABLE_ACTIVITY_VIEWS, SYNC_VIEW_CONTAINER_ID, SYNC_TITLE } from 'vs/workbench/services/userDataSync/common/userDataSync'; import { AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/editor/common/modes'; 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 { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { getAuthenticationProviderActivationEvent, 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'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { localize } from 'vs/nls'; @@ -30,6 +29,9 @@ import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/ import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IViewsService, ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views'; +import { isNative } from 'vs/base/common/platform'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; type UserAccountClassification = { id: { classification: 'EndUserPseudonymizedInformation', purpose: 'BusinessInsight' }; @@ -64,7 +66,11 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat private static DONOT_USE_WORKBENCH_SESSION_STORAGE_KEY = 'userDataSyncAccount.donotUseWorkbenchSession'; private static CACHED_SESSION_STORAGE_KEY = 'userDataSyncAccountPreference'; - readonly authenticationProviders: IAuthenticationProvider[]; + private _authenticationProviders: IAuthenticationProvider[] = []; + get enabled() { return this._authenticationProviders.length > 0; } + + private availableAuthenticationProviders: IAuthenticationProvider[] = []; + get authenticationProviders() { return this.availableAuthenticationProviders; } private _accountStatus: AccountStatus = AccountStatus.Uninitialized; get accountStatus(): AccountStatus { return this._accountStatus; } @@ -79,7 +85,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat private readonly syncEnablementContext: IContextKey; private readonly syncStatusContext: IContextKey; private readonly accountStatusContext: IContextKey; - private readonly manualSyncViewEnablementContext: IContextKey; + private readonly mergesViewEnablementContext: IContextKey; private readonly activityViewsEnablementContext: IContextKey; readonly userDataSyncPreview: UserDataSyncPreview = this._register(new UserDataSyncPreview(this.userDataSyncService)); @@ -93,9 +99,8 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, - @IProductService productService: IProductService, - @IConfigurationService configurationService: IConfigurationService, - @IExtensionService extensionService: IExtensionService, + @IProductService private readonly productService: IProductService, + @IExtensionService private readonly extensionService: IExtensionService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @INotificationService private readonly notificationService: INotificationService, @IProgressService private readonly progressService: IProgressService, @@ -103,37 +108,50 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat @IContextKeyService contextKeyService: IContextKeyService, @IViewsService private readonly viewsService: IViewsService, @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, + @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, + @IHostService private readonly hostService: IHostService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, ) { super(); - this.authenticationProviders = getUserDataSyncStore(productService, configurationService)?.authenticationProviders || []; + this._authenticationProviders = this.userDataSyncStoreManagementService.userDataSyncStore?.authenticationProviders || []; this.syncEnablementContext = CONTEXT_SYNC_ENABLEMENT.bindTo(contextKeyService); this.syncStatusContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService); this.accountStatusContext = CONTEXT_ACCOUNT_STATE.bindTo(contextKeyService); this.activityViewsEnablementContext = CONTEXT_ENABLE_ACTIVITY_VIEWS.bindTo(contextKeyService); - this.manualSyncViewEnablementContext = CONTEXT_ENABLE_MANUAL_SYNC_VIEW.bindTo(contextKeyService); - - if (this.authenticationProviders.length) { + this.mergesViewEnablementContext = CONTEXT_ENABLE_SYNC_MERGES_VIEW.bindTo(contextKeyService); + if (this._authenticationProviders.length) { this.syncStatusContext.set(this.userDataSyncService.status); this._register(userDataSyncService.onDidChangeStatus(status => this.syncStatusContext.set(status))); this.syncEnablementContext.set(userDataAutoSyncService.isEnabled()); this._register(userDataAutoSyncService.onDidChangeEnablement(enabled => this.syncEnablementContext.set(enabled))); - extensionService.whenInstalledExtensionsRegistered().then(() => { - if (this.authenticationProviders.every(({ id }) => authenticationService.isAuthenticationProviderRegistered(id))) { - this.initialize(); - } else { - const disposable = this.authenticationService.onDidRegisterAuthenticationProvider(() => { - if (this.authenticationProviders.every(({ id }) => authenticationService.isAuthenticationProviderRegistered(id))) { - disposable.dispose(); - this.initialize(); - } - }); - } - }); + this.waitAndInitialize(); } } + private isSupportedAuthenticationProviderId(authenticationProviderId: string): boolean { + return this._authenticationProviders.some(({ id }) => id === authenticationProviderId); + } + + private async waitAndInitialize(): Promise { + await this.extensionService.whenInstalledExtensionsRegistered(); + + /* activate unregistered providers */ + const unregisteredProviders = this._authenticationProviders.filter(({ id }) => !this.authenticationService.isAuthenticationProviderRegistered(id)); + if (unregisteredProviders.length) { + 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)))); + } + + /* initialize */ + await this.initialize(); + } + private async initialize(): Promise { if (this.currentSessionId === undefined && this.useWorkbenchSessionId && this.environmentService.options?.authenticationSessionId) { this.currentSessionId = this.environmentService.options.authenticationSessionId; @@ -158,8 +176,11 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat } private async update(): Promise { + + this.availableAuthenticationProviders = this._authenticationProviders.filter(({ id }) => this.authenticationService.isAuthenticationProviderRegistered(id)); + const allAccounts: Map = new Map(); - for (const { id } of this.authenticationProviders) { + for (const { id } of this.availableAuthenticationProviders) { const accounts = await this.getAccounts(id); allAccounts.set(id, accounts); } @@ -167,7 +188,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat this._all = allAccounts; const current = this.current; await this.updateToken(current); - this.updateAccountStatus(current); + this.updateAccountStatus(current ? AccountStatus.Available : AccountStatus.Unavailable); } private async getAccounts(authenticationProviderId: string): Promise { @@ -195,9 +216,9 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat let value: { token: string, authenticationProviderId: string } | undefined = undefined; if (current) { try { - this.logService.trace('Preferences Sync: Updating the token for the account', current.accountName); + this.logService.trace('Settings Sync: Updating the token for the account', current.accountName); const token = current.token; - this.logService.trace('Preferences Sync: Token updated for the account', current.accountName); + this.logService.trace('Settings Sync: Token updated for the account', current.accountName); value = { token, authenticationProviderId: current.authenticationProviderId }; } catch (e) { this.logService.error(e); @@ -206,10 +227,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat await this.userDataSyncAccountService.updateAccount(value); } - private updateAccountStatus(current: UserDataSyncAccount | undefined): void { - // set status - const accountStatus: AccountStatus = current ? AccountStatus.Available : AccountStatus.Unavailable; - + private updateAccountStatus(accountStatus: AccountStatus): void { if (this._accountStatus !== accountStatus) { const previous = this._accountStatus; this.logService.debug('Sync account status changed', previous, accountStatus); @@ -221,6 +239,13 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat } async turnOn(): Promise { + if (this.userDataAutoSyncService.isEnabled()) { + return; + } + if (this.userDataSyncService.status !== SyncStatus.Idle) { + throw new Error('Cannont turn on sync while syncing'); + } + const picked = await this.pick(); if (!picked) { throw canceled(); @@ -231,9 +256,17 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat throw new Error(localize('no account', "No account available")); } - const preferencesSyncTitle = localize('preferences sync', "Preferences Sync"); - const title = `${preferencesSyncTitle} [(${localize('details', "details")})](command:${SHOW_SYNC_LOG_COMMAND_ID})`; - await this.syncBeforeTurningOn(title); + const syncTitle = SYNC_TITLE; + const title = `${syncTitle} [(${localize('details', "details")})](command:${SHOW_SYNC_LOG_COMMAND_ID})`; + const manualSyncTask = await this.userDataSyncService.createManualSyncTask(); + const disposable = this.lifecycleService.onBeforeShutdown(e => e.veto(this.onBeforeShutdown(manualSyncTask))); + + try { + await this.syncBeforeTurningOn(title, manualSyncTask); + } finally { + disposable.dispose(); + } + await this.userDataAutoSyncService.turnOn(); this.notificationService.info(localize('sync turned on', "{0} is turned on", title)); } @@ -242,15 +275,27 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat return this.userDataAutoSyncService.turnOff(everywhere); } - private async syncBeforeTurningOn(title: string): Promise { + private async onBeforeShutdown(manualSyncTask: IManualSyncTask): Promise { + const result = await this.dialogService.confirm({ + type: 'warning', + message: localize('sync in progress', "Settings Sync is being turned on. Would you like to cancel it?"), + title: localize('settings sync', "Settings Sync"), + primaryButton: localize('yes', "Yes"), + secondaryButton: localize('no', "No"), + }); + if (result.confirmed) { + await manualSyncTask.stop(); + } + return !result.confirmed; + } + + private async syncBeforeTurningOn(title: string, manualSyncTask: IManualSyncTask): Promise { /* Make sure sync started on clean local state */ await this.userDataSyncService.resetLocal(); - const manualSyncTask = await this.userDataSyncService.createManualSyncTask(); try { let action: FirstTimeSyncAction = 'manual'; - let preview: [SyncResource, ISyncResourcePreview][] = []; await this.progressService.withProgress({ location: ProgressLocation.Notification, @@ -259,18 +304,24 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat }, async progress => { progress.report({ message: localize('turning on', "Turning on...") }); - preview = await manualSyncTask.preview(); + const preview = await manualSyncTask.preview(); const hasRemoteData = manualSyncTask.manifest !== null; const hasLocalData = await this.userDataSyncService.hasLocalData(); - const hasChanges = preview.some(([, { resourcePreviews }]) => resourcePreviews.some(r => r.localChange !== Change.None || r.remoteChange !== Change.None)); - const isLastSyncFromCurrentMachine = preview.every(([, { isLastSyncFromCurrentMachine }]) => isLastSyncFromCurrentMachine); + const hasMergesFromAnotherMachine = preview.some(([syncResource, { isLastSyncFromCurrentMachine, resourcePreviews }]) => + syncResource !== SyncResource.GlobalState && !isLastSyncFromCurrentMachine + && resourcePreviews.some(r => r.localChange !== Change.None || r.remoteChange !== Change.None)); - action = await this.getFirstTimeSyncAction(hasRemoteData, hasLocalData, hasChanges, isLastSyncFromCurrentMachine); + action = await this.getFirstTimeSyncAction(hasRemoteData, hasLocalData, hasMergesFromAnotherMachine); const progressDisposable = manualSyncTask.onSynchronizeResources(synchronizingResources => synchronizingResources.length ? progress.report({ message: localize('syncing resource', "Syncing {0}...", getSyncAreaLabel(synchronizingResources[0][0])) }) : undefined); try { switch (action) { - case 'merge': return await manualSyncTask.apply(); + case 'merge': + await manualSyncTask.merge(); + if (manualSyncTask.status !== SyncStatus.HasConflicts) { + await manualSyncTask.apply(); + } + return; case 'pull': return await manualSyncTask.pull(); case 'push': return await manualSyncTask.push(); case 'manual': return; @@ -279,8 +330,20 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat progressDisposable.dispose(); } }); + if (manualSyncTask.status === SyncStatus.HasConflicts) { + await this.dialogService.show( + Severity.Warning, + localize('conflicts detected', "Conflicts Detected"), + [localize('merge Manually', "Merge Manually...")], + { + detail: localize('resolve', "Unable to merge due to conflicts. Please merge manually to continue..."), + } + ); + await manualSyncTask.discardConflicts(); + action = 'manual'; + } if (action === 'manual') { - await this.syncManually(manualSyncTask, preview); + await this.syncManually(manualSyncTask); } } catch (error) { await manualSyncTask.stop(); @@ -290,28 +353,27 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat } } - private async getFirstTimeSyncAction(hasRemoteData: boolean, hasLocalData: boolean, hasChanges: boolean, isLastSyncFromCurrentMachine: boolean): Promise { + private async getFirstTimeSyncAction(hasRemoteData: boolean, hasLocalData: boolean, hasMergesFromAnotherMachine: boolean): Promise { if (!hasLocalData /* no data on local */ || !hasRemoteData /* no data on remote */ - || !hasChanges /* no changes */ - || isLastSyncFromCurrentMachine /* has changes but last sync is from current machine */ + || !hasMergesFromAnotherMachine /* no merges with another machine */ ) { return 'merge'; } const result = await this.dialogService.show( Severity.Info, - localize('Replace or Merge', "Replace or Merge"), + localize('merge or replace', "Merge or Replace"), [ localize('merge', "Merge"), localize('replace local', "Replace Local"), - localize('sync manually', "Sync Manually"), + localize('merge Manually', "Merge Manually..."), localize('cancel', "Cancel"), ], { cancelId: 3, - detail: localize('first time sync detail', "It looks like you last synced from another machine.\nWould you like to replace or merge with the synced data?"), + detail: localize('first time sync detail', "It looks like you last synced from another machine.\nWould you like to merge or replace with your data in the cloud?"), } ); switch (result.choice) { @@ -329,34 +391,35 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat throw canceled(); } - private async syncManually(task: IManualSyncTask, preview: [SyncResource, ISyncResourcePreview][]): Promise { + private async syncManually(task: IManualSyncTask): Promise { const visibleViewContainer = this.viewsService.getVisibleViewContainer(ViewContainerLocation.Sidebar); + const preview = await task.preview(); this.userDataSyncPreview.setManualSyncPreview(task, preview); - this.manualSyncViewEnablementContext.set(true); + this.mergesViewEnablementContext.set(true); await this.waitForActiveSyncViews(); - await this.viewsService.openView(MANUAL_SYNC_VIEW_ID); + await this.viewsService.openView(SYNC_MERGES_VIEW_ID); - const completed = await Event.toPromise(this.userDataSyncPreview.onDidCompleteManualSync); + const error = await Event.toPromise(this.userDataSyncPreview.onDidCompleteManualSync); this.userDataSyncPreview.unsetManualSyncPreview(); - this.manualSyncViewEnablementContext.set(false); + this.mergesViewEnablementContext.set(false); if (visibleViewContainer) { this.viewsService.openViewContainer(visibleViewContainer.id); } else { - const viewContainer = this.viewDescriptorService.getViewContainerByViewId(MANUAL_SYNC_VIEW_ID); + const viewContainer = this.viewDescriptorService.getViewContainerByViewId(SYNC_MERGES_VIEW_ID); this.viewsService.closeViewContainer(viewContainer!.id); } - if (!completed) { - throw canceled(); + if (error) { + throw error; } } async resetSyncedData(): Promise { const result = await this.dialogService.confirm({ - message: localize('reset', "This will clear your synced data from the cloud and stop sync on all your devices."), - title: localize('reset title', "Reset Synced Data"), + message: localize('reset', "This will clear your data in the cloud and stop sync on all your devices."), + title: localize('reset title', "Clear"), type: 'info', primaryButton: localize('reset button', "Reset"), }); @@ -371,6 +434,30 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat await this.viewsService.openViewContainer(SYNC_VIEW_CONTAINER_ID); } + async switchSyncService(type: UserDataSyncStoreType): Promise { + if (!this.userDataSyncStoreManagementService.userDataSyncStore + || !this.userDataSyncStoreManagementService.userDataSyncStore.insidersUrl + || !this.userDataSyncStoreManagementService.userDataSyncStore.stableUrl) { + return; + } + await this.userDataSyncStoreManagementService.switch(type); + const res = await this.dialogService.confirm({ + type: 'info', + message: isNative ? + localize('relaunchMessage', "Switching settings sync service requires a restart to take effect.") : + localize('relaunchMessageWeb', "Switching settings sync service requires a reload to take effect."), + detail: isNative ? + localize('relaunchDetail', "Press the restart button to restart {0} and switch.", this.productService.nameLong) : + localize('relaunchDetailWeb', "Press the reload button to reload {0} and switch.", this.productService.nameLong), + primaryButton: isNative ? + localize('restart', "&&Restart") : + localize('restartWeb', "&&Reload"), + }); + if (res.confirmed) { + this.hostService.restart(); + } + } + private async waitForActiveSyncViews(): Promise { const viewContainer = this.viewDescriptorService.getViewContainerById(SYNC_VIEW_CONTAINER_ID); if (viewContainer) { @@ -381,10 +468,6 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat } } - private isSupportedAuthenticationProviderId(authenticationProviderId: string): boolean { - return this.authenticationProviders.some(({ id }) => id === authenticationProviderId); - } - private isCurrentAccount(account: UserDataSyncAccount): boolean { return account.sessionId === this.currentSessionId; } @@ -414,15 +497,15 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat } private async doPick(): Promise { - if (this.authenticationProviders.length === 0) { + if (this.availableAuthenticationProviders.length === 0) { return undefined; } await this.update(); // Single auth provider and no accounts available - if (this.authenticationProviders.length === 1 && !this.all.length) { - return this.authenticationProviders[0]; + if (this.availableAuthenticationProviders.length === 1 && !this.all.length) { + return this.availableAuthenticationProviders[0]; } return new Promise(async (c, e) => { @@ -431,7 +514,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat const quickPick = this.quickInputService.createQuickPick(); disposables.add(quickPick); - quickPick.title = localize('pick an account', "Preferences Sync"); + quickPick.title = SYNC_TITLE; quickPick.ok = false; quickPick.placeholder = localize('choose account placeholder', "Select an account"); quickPick.ignoreFocusOut = true; @@ -454,7 +537,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat // Signed in Accounts if (this.all.length) { - const authenticationProviders = [...this.authenticationProviders].sort(({ id }) => id === this.current?.authenticationProviderId ? -1 : 1); + const authenticationProviders = [...this.availableAuthenticationProviders].sort(({ id }) => id === this.current?.authenticationProviderId ? -1 : 1); quickPickItems.push({ type: 'separator', label: localize('signed in', "Signed in") }); for (const authenticationProvider of authenticationProviders) { const accounts = (this._all.get(authenticationProvider.id) || []).sort(({ sessionId }) => sessionId === this.current?.sessionId ? -1 : 1); @@ -472,7 +555,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat } // Account proviers - for (const authenticationProvider of this.authenticationProviders) { + for (const authenticationProvider of this.availableAuthenticationProviders) { const signedInForProvider = this.all.some(account => account.authenticationProviderId === authenticationProvider.id); if (!signedInForProvider || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) { const providerName = this.authenticationService.getLabel(authenticationProvider.id); @@ -500,7 +583,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat this.notificationService.notify({ severity: Severity.Error, - message: localize('successive auth failures', "Preferences sync was turned off because of successive authorization failures. Please sign in again to continue synchronizing"), + message: localize('successive auth failures', "Settings sync was turned off because of successive authorization failures. Please sign in again to continue synchronizing"), actions: { primary: [new Action('sign in', localize('sign in', "Sign in"), undefined, true, () => this.signIn())] } @@ -567,7 +650,7 @@ class UserDataSyncPreview extends Disposable implements IUserDataSyncPreview { private _onDidChangeConflicts = this._register(new Emitter>()); readonly onDidChangeConflicts = this._onDidChangeConflicts.event; - private _onDidCompleteManualSync = this._register(new Emitter()); + private _onDidCompleteManualSync = this._register(new Emitter()); readonly onDidCompleteManualSync = this._onDidCompleteManualSync.event; private manualSync: { preview: [SyncResource, ISyncResourcePreview][], task: IManualSyncTask, disposables: DisposableStore } | undefined; @@ -593,7 +676,7 @@ class UserDataSyncPreview extends Disposable implements IUserDataSyncPreview { this.updateResources(); } - async accept(syncResource: SyncResource, resource: URI, content: string): Promise { + async accept(syncResource: SyncResource, resource: URI, content?: string | null): Promise { if (this.manualSync) { const syncPreview = await this.manualSync.task.accept(resource, content); this.updatePreview(syncPreview); @@ -623,10 +706,16 @@ class UserDataSyncPreview extends Disposable implements IUserDataSyncPreview { throw new Error('Can apply only while syncing manually'); } - const syncPreview = await this.manualSync.task.apply(); - this.updatePreview(syncPreview); - if (!this._resources.length) { - this._onDidCompleteManualSync.fire(true); + try { + const syncPreview = await this.manualSync.task.apply(); + this.updatePreview(syncPreview); + if (!this._resources.length) { + this._onDidCompleteManualSync.fire(undefined); + } + } catch (error) { + await this.manualSync.task.stop(); + this.updatePreview([]); + this._onDidCompleteManualSync.fire(error); } } @@ -636,7 +725,7 @@ class UserDataSyncPreview extends Disposable implements IUserDataSyncPreview { } await this.manualSync.task.stop(); this.updatePreview([]); - this._onDidCompleteManualSync.fire(false); + this._onDidCompleteManualSync.fire(canceled()); } async pull(): Promise { diff --git a/src/vs/workbench/services/userDataSync/common/userDataSync.ts b/src/vs/workbench/services/userDataSync/common/userDataSync.ts index 1bac7f1a6c4..aaaad9acf40 100644 --- a/src/vs/workbench/services/userDataSync/common/userDataSync.ts +++ b/src/vs/workbench/services/userDataSync/common/userDataSync.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IAuthenticationProvider, SyncStatus, SyncResource, Change, MergeState } from 'vs/platform/userDataSync/common/userDataSync'; +import { IAuthenticationProvider, SyncStatus, SyncResource, Change, MergeState, UserDataSyncStoreType } from 'vs/platform/userDataSync/common/userDataSync'; import { Event } from 'vs/base/common/event'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { localize } from 'vs/nls'; @@ -20,7 +20,7 @@ export interface IUserDataSyncPreview { readonly onDidChangeResources: Event>; readonly resources: ReadonlyArray; - accept(syncResource: SyncResource, resource: URI, content: string): Promise; + accept(syncResource: SyncResource, resource: URI, content?: string | null): Promise; merge(resource?: URI): Promise; discard(resource?: URI): Promise; pull(): Promise; @@ -44,7 +44,9 @@ export const IUserDataSyncWorkbenchService = createDecorator; turnoff(everyWhere: boolean): Promise; signIn(): Promise; + switchSyncService(type: UserDataSyncStoreType): Promise; resetSyncedData(): Promise; showSyncActivity(): Promise; @@ -77,12 +80,14 @@ export const enum AccountStatus { Available = 'available', } +export const SYNC_TITLE = localize('sync category', "Settings Sync"); + // Contexts export const CONTEXT_SYNC_STATE = new RawContextKey('syncStatus', SyncStatus.Uninitialized); export const CONTEXT_SYNC_ENABLEMENT = new RawContextKey('syncEnabled', false); export const CONTEXT_ACCOUNT_STATE = new RawContextKey('userDataSyncAccountStatus', AccountStatus.Uninitialized); export const CONTEXT_ENABLE_ACTIVITY_VIEWS = new RawContextKey(`enableSyncActivityViews`, false); -export const CONTEXT_ENABLE_MANUAL_SYNC_VIEW = new RawContextKey(`enableManualSyncView`, false); +export const CONTEXT_ENABLE_SYNC_MERGES_VIEW = new RawContextKey(`enableSyncMergesView`, false); // Commands export const CONFIGURE_SYNC_COMMAND_ID = 'workbench.userDataSync.actions.configure'; @@ -90,4 +95,4 @@ export const SHOW_SYNC_LOG_COMMAND_ID = 'workbench.userDataSync.actions.showLog' // VIEWS export const SYNC_VIEW_CONTAINER_ID = 'workbench.view.sync'; -export const MANUAL_SYNC_VIEW_ID = 'workbench.views.manualSyncView'; +export const SYNC_MERGES_VIEW_ID = 'workbench.views.sync.merges'; diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncMachinesService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncMachinesService.ts index d9320052d1d..44dbc9e85e1 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncMachinesService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncMachinesService.ts @@ -8,6 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IUserDataSyncMachinesService, IUserDataSyncMachine } from 'vs/platform/userDataSync/common/userDataSyncMachines'; +import { Event } from 'vs/base/common/event'; class UserDataSyncMachinesService extends Disposable implements IUserDataSyncMachinesService { @@ -15,6 +16,8 @@ class UserDataSyncMachinesService extends Disposable implements IUserDataSyncMac private readonly channel: IChannel; + get onDidChange(): Event { return this.channel.listen('onDidChange'); } + constructor( @ISharedProcessService sharedProcessService: ISharedProcessService ) { diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts index dbe45d1ab02..638cda9d60d 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts @@ -38,6 +38,9 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ private _onSyncErrors: Emitter<[SyncResource, UserDataSyncError][]> = this._register(new Emitter<[SyncResource, UserDataSyncError][]>()); readonly onSyncErrors: Event<[SyncResource, UserDataSyncError][]> = this._onSyncErrors.event; + get onDidResetLocal(): Event { return this.channel.listen('onDidResetLocal'); } + get onDidResetRemote(): Event { return this.channel.listen('onDidResetRemote'); } + constructor( @ISharedProcessService private readonly sharedProcessService: ISharedProcessService ) { @@ -65,17 +68,13 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this._register(this.channel.listen<[SyncResource, Error][]>('onSyncErrors')(errors => this._onSyncErrors.fire(errors.map(([source, error]) => ([source, UserDataSyncError.toUserDataSyncError(error)]))))); } - pull(): Promise { - return this.channel.call('pull'); - } - createSyncTask(): Promise { throw new Error('not supported'); } async createManualSyncTask(): Promise { - const { id, manifest } = await this.channel.call<{ id: string, manifest: IUserDataManifest | null }>('createManualSyncTask'); - return new ManualSyncTask(id, manifest, this.sharedProcessService); + const { id, manifest, status } = await this.channel.call<{ id: string, manifest: IUserDataManifest | null, status: SyncStatus }>('createManualSyncTask'); + return new ManualSyncTask(id, manifest, status, this.sharedProcessService); } replace(uri: URI): Promise { @@ -102,7 +101,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return this.channel.call('hasLocalData'); } - accept(syncResource: SyncResource, resource: URI, content: string, apply: boolean): Promise { + accept(syncResource: SyncResource, resource: URI, content: string | null, apply: boolean): Promise { return this.channel.call('accept', [syncResource, resource, content, apply]); } @@ -120,8 +119,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return handles.map(({ created, uri }) => ({ created, uri: URI.revive(uri) })); } - async getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { - const result = await this.channel.call<{ resource: URI, comparableResource?: URI }[]>('getAssociatedResources', [resource, syncResourceHandle]); + async getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> { + const result = await this.channel.call<{ resource: URI, comparableResource: URI }[]>('getAssociatedResources', [resource, syncResourceHandle]); return result.map(({ resource, comparableResource }) => ({ resource: URI.revive(resource), comparableResource: URI.revive(comparableResource) })); } @@ -164,16 +163,27 @@ class ManualSyncTask implements IManualSyncTask { get onSynchronizeResources(): Event<[SyncResource, URI[]][]> { return this.channel.listen<[SyncResource, URI[]][]>('onSynchronizeResources'); } + private _status: SyncStatus; + get status(): SyncStatus { return this._status; } + constructor( readonly id: string, readonly manifest: IUserDataManifest | null, + status: SyncStatus, sharedProcessService: ISharedProcessService, ) { const manualSyncTaskChannel = sharedProcessService.getChannel(`manualSyncTask-${id}`); + this._status = status; + const that = this; this.channel = { - call(command: string, arg?: any, cancellationToken?: CancellationToken): Promise { - return manualSyncTaskChannel.call(command, arg, cancellationToken) - .then(null, error => { throw UserDataSyncError.toUserDataSyncError(error); }); + async call(command: string, arg?: any, cancellationToken?: CancellationToken): Promise { + try { + const result = await manualSyncTaskChannel.call(command, arg, cancellationToken); + that._status = await manualSyncTaskChannel.call('_getStatus'); + return result; + } catch (error) { + throw UserDataSyncError.toUserDataSyncError(error); + } }, listen(event: string, arg?: any): Event { return manualSyncTaskChannel.listen(event, arg); @@ -186,12 +196,12 @@ class ManualSyncTask implements IManualSyncTask { return this.deserializePreviews(previews); } - async accept(resource: URI, content: string): Promise<[SyncResource, ISyncResourcePreview][]> { + async accept(resource: URI, content?: string | null): Promise<[SyncResource, ISyncResourcePreview][]> { const previews = await this.channel.call<[SyncResource, ISyncResourcePreview][]>('accept', [resource, content]); return this.deserializePreviews(previews); } - async merge(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]> { + async merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]> { const previews = await this.channel.call<[SyncResource, ISyncResourcePreview][]>('merge', [resource]); return this.deserializePreviews(previews); } @@ -201,6 +211,11 @@ class ManualSyncTask implements IManualSyncTask { return this.deserializePreviews(previews); } + async discardConflicts(): Promise<[SyncResource, ISyncResourcePreview][]> { + const previews = await this.channel.call<[SyncResource, ISyncResourcePreview][]>('discardConflicts'); + return this.deserializePreviews(previews); + } + async apply(): Promise<[SyncResource, ISyncResourcePreview][]> { const previews = await this.channel.call<[SyncResource, ISyncResourcePreview][]>('apply'); return this.deserializePreviews(previews); diff --git a/src/vs/workbench/test/browser/api/extHostDiagnostics.test.ts b/src/vs/workbench/test/browser/api/extHostDiagnostics.test.ts index 1bb7bafe7ca..db7351cfcb7 100644 --- a/src/vs/workbench/test/browser/api/extHostDiagnostics.test.ts +++ b/src/vs/workbench/test/browser/api/extHostDiagnostics.test.ts @@ -389,6 +389,9 @@ suite('ExtHostDiagnostics', () => { assertRegistered(): void { } + drain() { + return undefined!; + } }, new NullLogService()); let collection1 = diags.createDiagnosticCollection(nullExtensionDescription.identifier, 'foo'); @@ -438,6 +441,9 @@ suite('ExtHostDiagnostics', () => { assertRegistered(): void { } + drain() { + return undefined!; + } }, new NullLogService()); diff --git a/src/vs/workbench/test/browser/api/extHostFileSystemEventService.test.ts b/src/vs/workbench/test/browser/api/extHostFileSystemEventService.test.ts index c20cee41ce1..0d26033abf7 100644 --- a/src/vs/workbench/test/browser/api/extHostFileSystemEventService.test.ts +++ b/src/vs/workbench/test/browser/api/extHostFileSystemEventService.test.ts @@ -15,7 +15,8 @@ suite('ExtHostFileSystemEventService', () => { const protocol: IMainContext = { getProxy: () => { return undefined!; }, set: undefined!, - assertRegistered: undefined! + assertRegistered: undefined!, + drain: undefined! }; const watcher1 = new ExtHostFileSystemEventService(protocol, new NullLogService(), undefined!).createFileSystemWatcher('**/somethingInteresting', false, false, false); diff --git a/src/vs/workbench/test/browser/api/extHostWorkspace.test.ts b/src/vs/workbench/test/browser/api/extHostWorkspace.test.ts index 6cbc2c7a930..7e9732b500a 100644 --- a/src/vs/workbench/test/browser/api/extHostWorkspace.test.ts +++ b/src/vs/workbench/test/browser/api/extHostWorkspace.test.ts @@ -298,7 +298,8 @@ suite('ExtHostWorkspace', function () { const protocol: IMainContext = { getProxy: () => { return undefined!; }, set: () => { return undefined!; }, - assertRegistered: () => { } + assertRegistered: () => { }, + drain: () => { return undefined!; }, }; const ws = createExtHostWorkspace(protocol, { id: 'foo', name: 'Test', folders: [] }, new NullLogService()); diff --git a/src/vs/workbench/test/browser/api/mainThreadDiagnostics.test.ts b/src/vs/workbench/test/browser/api/mainThreadDiagnostics.test.ts index 410665cd79b..3f10f0cffd6 100644 --- a/src/vs/workbench/test/browser/api/mainThreadDiagnostics.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadDiagnostics.test.ts @@ -32,6 +32,7 @@ suite('MainThreadDiagnostics', function () { $acceptMarkersChange() { } }; } + drain(): any { return null; } }, markerService, new class extends mock() { diff --git a/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts b/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts index 936a51b094f..e88fcd90b6d 100644 --- a/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts @@ -66,6 +66,7 @@ suite('MainThreadHostTreeView', function () { getProxy(): any { return extHostTreeViewsShape; } + drain(): any { return null; } }, new TestViewsService(), new TestNotificationService(), testExtensionService, new NullLogService()); mainThreadTreeViews.$registerTreeViewDataProvider(testTreeViewId, { showCollapseAll: false, canSelectMany: false }); await testExtensionService.whenInstalledExtensionsRegistered(); diff --git a/src/vs/workbench/test/browser/api/testRPCProtocol.ts b/src/vs/workbench/test/browser/api/testRPCProtocol.ts index 16673942bd6..d2d2b1c504f 100644 --- a/src/vs/workbench/test/browser/api/testRPCProtocol.ts +++ b/src/vs/workbench/test/browser/api/testRPCProtocol.ts @@ -19,7 +19,8 @@ export function SingleProxyRPCProtocol(thing: any): IExtHostContext & IExtHostRp set(identifier: ProxyIdentifier, value: R): R { return value; }, - assertRegistered: undefined! + assertRegistered: undefined!, + drain: undefined! }; } @@ -40,6 +41,10 @@ export class TestRPCProtocol implements IExtHostContext, IExtHostRpcService { this._proxies = Object.create(null); } + drain(): Promise { + return Promise.resolve(); + } + private get _callCount(): number { return this._callCountValue; } diff --git a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts index eab55d05b22..ed378fb9496 100644 --- a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts +++ b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts @@ -176,6 +176,9 @@ class TestTelemetryService implements ITelemetryService { public setEnabled(value: boolean): void { } + public setExperimentProperty(name: string, value: string): void { + } + public publicLog(eventName: string, data?: any): Promise { const event = { name: eventName, data: data }; this.events.push(event); diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index eb0221ab529..9e555a89207 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -194,6 +194,8 @@ export class TestElectronService implements IElectronService { async showItemInFolder(path: string): Promise { } async setRepresentedFilename(path: string): Promise { } async isAdmin(): Promise { return false; } + async getTotalMem(): Promise { return 0; } + async killProcess(): Promise { } async setDocumentEdited(edited: boolean): Promise { } async openExternal(url: string): Promise { return false; } async updateTouchBar(): Promise { } diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 1c13f30b47c..d52fd0e9c7a 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -58,11 +58,12 @@ import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncAccountS import 'vs/workbench/services/sharedProcess/electron-browser/sharedProcessService'; import 'vs/workbench/services/localizations/electron-browser/localizationsService'; import 'vs/workbench/services/path/electron-browser/pathService'; +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 } from 'vs/platform/userDataSync/common/userDataSync'; +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'; @@ -70,6 +71,7 @@ import { ITimerService } from 'vs/workbench/services/timer/browser/timerService' import { TimerService } from 'vs/workbench/services/timer/electron-browser/timerService'; registerSingleton(ICredentialsService, KeytarCredentialsService, true); +registerSingleton(IUserDataSyncStoreManagementService, UserDataSyncStoreManagementService); registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); registerSingleton(ITunnelService, TunnelService); registerSingleton(ITimerService, TimerService); @@ -133,5 +135,6 @@ 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.web.api.ts b/src/vs/workbench/workbench.web.api.ts index d3084f375b9..ce168ccdd0a 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -268,10 +268,25 @@ interface IWorkbenchConstructionOptions { readonly staticExtensions?: ReadonlyArray; /** + * [TEMPORARY]: This will be removed soon. * Service end-point hosting builtin extensions */ readonly builtinExtensionsServiceUrl?: string; + /** + * [TEMPORARY]: This will be removed soon. + * Enable inlined extensions. + * Defaults to false on serverful and true on serverless. + */ + readonly _enableBuiltinExtensions?: boolean; + + /** + * [TEMPORARY]: This will be removed soon. + * Enable `