diff --git a/.devcontainer/cache/build-cache-image.sh b/.devcontainer/cache/build-cache-image.sh index 78d0fbfdf0c..42e143d7af4 100755 --- a/.devcontainer/cache/build-cache-image.sh +++ b/.devcontainer/cache/build-cache-image.sh @@ -8,7 +8,7 @@ set -e SCRIPT_PATH="$(cd "$(dirname $0)" && pwd)" CONTAINER_IMAGE_REPOSITORY="$1" -BRANCH="${2:-"master"}" +BRANCH="${2:-"main"}" if [ "${CONTAINER_IMAGE_REPOSITORY}" = "" ]; then echo "Container repository not specified!" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index cd632e134ef..3b82cd9028d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,7 @@ "name": "Code - OSS", // Image contents: https://github.com/microsoft/vscode-dev-containers/blob/master/repository-containers/images/github.com/microsoft/vscode/.devcontainer/base.Dockerfile - "image": "mcr.microsoft.com/vscode/devcontainers/repos/microsoft/vscode:branch-master", + "image": "mcr.microsoft.com/vscode/devcontainers/repos/microsoft/vscode:branch-main", "workspaceMount": "source=${localWorkspaceFolder},target=/home/node/workspace/vscode,type=bind,consistency=cached", "workspaceFolder": "/home/node/workspace/vscode", diff --git a/.eslintrc.json b/.eslintrc.json index cf4c1418a11..09fa5a59cde 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -278,6 +278,7 @@ "sinon", "vs/nls", "**/vs/base/{common,browser}/**", + "**/vs/base/parts/*/{common,browser}/**", "**/vs/platform/*/{common,browser}/**", "**/vs/platform/*/test/{common,browser}/**" ] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 45f9ee8f340..19314029215 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,7 +2,7 @@ * Read our Pull Request guidelines: https://github.com/microsoft/vscode/wiki/How-to-Contribute#pull-requests * Associate an issue with the Pull Request. -* Ensure that the code is up-to-date with the `master` branch. +* Ensure that the code is up-to-date with the `main` branch. * Include a description of the proposed changes and how to test them. --> diff --git a/.github/workflows/build-chat.yml b/.github/workflows/build-chat.yml index f9f146164ba..03f3bd42caf 100644 --- a/.github/workflows/build-chat.yml +++ b/.github/workflows/build-chat.yml @@ -7,7 +7,7 @@ on: types: - completed branches: - - master + - main - release/* jobs: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d53283f0cf4..f1324b59bc0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,11 +3,11 @@ name: CI on: push: branches: - - master + - main - release/* pull_request: branches: - - master + - main - release/* jobs: diff --git a/.github/workflows/devcontainer-cache.yml b/.github/workflows/devcontainer-cache.yml index a250b56cd7a..4670186bdda 100644 --- a/.github/workflows/devcontainer-cache.yml +++ b/.github/workflows/devcontainer-cache.yml @@ -2,9 +2,9 @@ name: VS Code Repo Dev Container Cache Image Generation on: push: - # Currently doing this for master, but could be done for PRs as well + # Currently doing this for main, but could be done for PRs as well branches: - - "master" + - "main" # Only updates to these files result in changes to installed packages, so skip otherwise paths: @@ -35,6 +35,6 @@ jobs: az acr login --name $ACR_REGISTRY_NAME GIT_BRANCH=$(echo "${{ github.ref }}" | grep -oP 'refs/(heads|tags)/\K(.+)') - if [ "$GIT_BRANCH" == "" ]; then GIT_BRANCH=master; fi + if [ "$GIT_BRANCH" == "" ]; then GIT_BRANCH=main; fi .devcontainer/cache/build-cache-image.sh "${{ secrets.CONTAINER_IMAGE_REGISTRY }}/public/vscode/devcontainers/repos/microsoft/vscode" "${GIT_BRANCH}" diff --git a/.github/workflows/rich-navigation.yml b/.github/workflows/rich-navigation.yml index aee0796fa28..71824cab83d 100644 --- a/.github/workflows/rich-navigation.yml +++ b/.github/workflows/rich-navigation.yml @@ -3,7 +3,7 @@ on: pull_request: push: branches: - - master + - main jobs: richnav: diff --git a/.vscode/launch.json b/.vscode/launch.json index 11c578b992d..b1db49c2549 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -110,7 +110,8 @@ // "${workspaceFolder}", // Uncomment for running out of sources. "${workspaceFolder}/extensions/vscode-api-tests/testWorkspace", "--extensionDevelopmentPath=${workspaceFolder}/extensions/vscode-api-tests", - "--extensionTestsPath=${workspaceFolder}/extensions/vscode-api-tests/out/singlefolder-tests" + "--extensionTestsPath=${workspaceFolder}/extensions/vscode-api-tests/out/singlefolder-tests", + "--disable-extensions" ], "outFiles": [ "${workspaceFolder}/out/**/*.js" diff --git a/.vscode/notebooks/inbox.github-issues b/.vscode/notebooks/inbox.github-issues index f14b59f1483..be0a2609cb2 100644 --- a/.vscode/notebooks/inbox.github-issues +++ b/.vscode/notebooks/inbox.github-issues @@ -30,7 +30,7 @@ { "kind": 1, "language": "markdown", - "value": "New issues or pull requests submitted by the community are initially triaged by an [automatic classification bot](https://github.com/microsoft/vscode-github-triage-actions/tree/master/classifier-deep). Issues that the bot does not correctly triage are then triaged by a team member. The team rotates the inbox tracker on a weekly basis.\n\nA [mirror](https://github.com/JacksonKearl/testissues/issues) of the VS Code issue stream is available with details about how the bot classifies issues, including feature-area classifications and confidence ratings. Per-category confidence thresholds and feature-area ownership data is maintained in [.github/classifier.json](https://github.com/microsoft/vscode/blob/master/.github/classifier.json). \n\nšŸ’” The bot is being run through a GitHub action that runs every 30 minutes. Give the bot the opportunity to classify an issue before doing it manually.\n\n### Inbox Tracking\n\nThe inbox tracker is responsible for the [global inbox](https://github.com/microsoft/vscode/issues?utf8=%E2%9C%93&q=is%3Aopen+no%3Aassignee+-label%3Afeature-request+-label%3Atestplan-item+-label%3Aplan-item) containing all **open issues and pull requests** that\n- are neither **feature requests** nor **test plan items** nor **plan items** and\n- have **no owner assignment**.\n\nThe **inbox tracker** may perform any step described in our [issue triaging documentation](https://github.com/microsoft/vscode/wiki/Issues-Triaging) but its main responsibility is to route issues to the actual feature area owner.\n\nFeature area owners track the **feature area inbox** containing all **open issues and pull requests** that\n- are personally assigned to them and are not assigned to any milestone\n- are labeled with their feature area label and are not assigned to any milestone.\nThis secondary triage may involve any of the steps described in our [issue triaging documentation](https://github.com/microsoft/vscode/wiki/Issues-Triaging) and results in a fully triaged or closed issue.\n\nThe [github triage extension](https://github.com/microsoft/vscode-github-triage-extension) can be used to assist with triaging — it provides a \"Command Palette\"-style list of triaging actions like assignment, labeling, and triggers for various bot actions.", + "value": "New issues or pull requests submitted by the community are initially triaged by an [automatic classification bot](https://github.com/microsoft/vscode-github-triage-actions/tree/master/classifier-deep). Issues that the bot does not correctly triage are then triaged by a team member. The team rotates the inbox tracker on a weekly basis.\n\nA [mirror](https://github.com/JacksonKearl/testissues/issues) of the VS Code issue stream is available with details about how the bot classifies issues, including feature-area classifications and confidence ratings. Per-category confidence thresholds and feature-area ownership data is maintained in [.github/classifier.json](https://github.com/microsoft/vscode/blob/main/.github/classifier.json). \n\nšŸ’” The bot is being run through a GitHub action that runs every 30 minutes. Give the bot the opportunity to classify an issue before doing it manually.\n\n### Inbox Tracking\n\nThe inbox tracker is responsible for the [global inbox](https://github.com/microsoft/vscode/issues?utf8=%E2%9C%93&q=is%3Aopen+no%3Aassignee+-label%3Afeature-request+-label%3Atestplan-item+-label%3Aplan-item) containing all **open issues and pull requests** that\n- are neither **feature requests** nor **test plan items** nor **plan items** and\n- have **no owner assignment**.\n\nThe **inbox tracker** may perform any step described in our [issue triaging documentation](https://github.com/microsoft/vscode/wiki/Issues-Triaging) but its main responsibility is to route issues to the actual feature area owner.\n\nFeature area owners track the **feature area inbox** containing all **open issues and pull requests** that\n- are personally assigned to them and are not assigned to any milestone\n- are labeled with their feature area label and are not assigned to any milestone.\nThis secondary triage may involve any of the steps described in our [issue triaging documentation](https://github.com/microsoft/vscode/wiki/Issues-Triaging) and results in a fully triaged or closed issue.\n\nThe [github triage extension](https://github.com/microsoft/vscode-github-triage-extension) can be used to assist with triaging — it provides a \"Command Palette\"-style list of triaging actions like assignment, labeling, and triggers for various bot actions.", "editable": true }, { diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index fb59e914e78..1502c29db16 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -8,7 +8,8 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog\n\n// current milestone name\n$milestone=milestone:\"February 2021\"" + "value": "// list of repos we work in\n$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog\n\n// current milestone name\n$milestone=milestone:\"February 2021\"", + "editable": true }, { "kind": 1, @@ -19,7 +20,8 @@ { "kind": 2, "language": "github-issues", - "value": "$repos $milestone assignee:@me is:open" + "value": "$repos $milestone assignee:@me is:open", + "editable": true }, { "kind": 1, @@ -36,7 +38,8 @@ { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open label:bug" + "value": "$repos assignee:@me is:open label:bug", + "editable": true }, { "kind": 1, @@ -47,7 +50,8 @@ { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open label:debt OR $repos assignee:@me is:open label:engineering" + "value": "$repos assignee:@me is:open label:debt OR $repos assignee:@me is:open label:engineering", + "editable": true }, { "kind": 1, @@ -58,7 +62,8 @@ { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open label:perf OR $repos assignee:@me is:open label:perf-startup OR $repos assignee:@me is:open label:perf-bloat OR $repos assignee:@me is:open label:freeze-slow-crash-leak" + "value": "$repos assignee:@me is:open label:perf OR $repos assignee:@me is:open label:perf-startup OR $repos assignee:@me is:open label:perf-bloat OR $repos assignee:@me is:open label:freeze-slow-crash-leak", + "editable": true }, { "kind": 1, @@ -69,12 +74,14 @@ { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open label:feature-request milestone:Backlog sort:reactions-+1-desc" + "value": "$repos assignee:@me is:open label:feature-request milestone:Backlog sort:reactions-+1-desc", + "editable": true }, { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open milestone:\"Backlog Candidates\"" + "value": "$repos assignee:@me is:open milestone:\"Backlog Candidates\"", + "editable": true }, { "kind": 1, @@ -91,7 +98,8 @@ { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open type:issue -label:bug -label:\"needs more info\" -label:feature-request -label:under-discussion -label:debt -label:plan-item -label:upstream" + "value": "$repos assignee:@me is:open type:issue -label:bug -label:\"needs more info\" -label:feature-request -label:under-discussion -label:debt -label:plan-item -label:upstream", + "editable": true }, { "kind": 1, @@ -102,6 +110,7 @@ { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open label:\"needs more info\"" + "value": "$repos assignee:@me is:open label:\"needs more info\"", + "editable": true } ] \ No newline at end of file diff --git a/README.md b/README.md index ec206f09460..541c6e3773c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ## The Repository -This repository ("`Code - OSS`") is where we (Microsoft) develop the [Visual Studio Code](https://code.visualstudio.com) product. Not only do we work on code and issues here, we also publish our [roadmap](https://github.com/microsoft/vscode/wiki/Roadmap), [monthly iteration plans](https://github.com/microsoft/vscode/wiki/Iteration-Plans), and our [endgame plans](https://github.com/microsoft/vscode/wiki/Running-the-Endgame). This source code is available to everyone under the standard [MIT license](https://github.com/microsoft/vscode/blob/master/LICENSE.txt). +This repository ("`Code - OSS`") is where we (Microsoft) develop the [Visual Studio Code](https://code.visualstudio.com) product. Not only do we work on code and issues here, we also publish our [roadmap](https://github.com/microsoft/vscode/wiki/Roadmap), [monthly iteration plans](https://github.com/microsoft/vscode/wiki/Iteration-Plans), and our [endgame plans](https://github.com/microsoft/vscode/wiki/Running-the-Endgame). This source code is available to everyone under the standard [MIT license](https://github.com/microsoft/vscode/blob/main/LICENSE.txt). ## Visual Studio Code diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index a34ae1c4247..3bbbbb461e1 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -71,6 +71,7 @@ steps: condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - script: | + set -e npx https://aka.ms/enablesecurefeed standAlone timeoutInMinutes: 5 condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), eq(variables['ENABLE_TERRAPIN'], 'true')) @@ -387,7 +388,3 @@ steps: displayName: Upload configuration (for Bing settings search) condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), ne(variables['VSCODE_PUBLISH'], 'false')) continueOnError: true - - - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 - displayName: "Component Detection" - continueOnError: true diff --git a/build/azure-pipelines/distro-build.yml b/build/azure-pipelines/distro-build.yml index 331fbf9675e..22d6983e7f8 100644 --- a/build/azure-pipelines/distro-build.yml +++ b/build/azure-pipelines/distro-build.yml @@ -1,9 +1,9 @@ trigger: branches: - include: ["master", "release/*"] + include: ["main", "release/*"] pr: branches: - include: ["master", "release/*"] + include: ["main", "release/*"] steps: - task: NodeTool@0 @@ -31,8 +31,8 @@ steps: git remote add distro "https://github.com/$VSCODE_MIXIN_REPO.git" git fetch distro - # Push master branch into oss/master - git push distro origin/master:refs/heads/oss/master + # Push main branch into oss/main + git push distro origin/main:refs/heads/oss/main # Push every release branch into oss/release git for-each-ref --format="%(refname:short)" refs/remotes/origin/release/* | sed 's/^origin\/\(.*\)$/\0:refs\/heads\/oss\/\1/' | xargs git push distro diff --git a/build/azure-pipelines/exploration-build.yml b/build/azure-pipelines/exploration-build.yml index 22e2602d0aa..719e6e469cb 100644 --- a/build/azure-pipelines/exploration-build.yml +++ b/build/azure-pipelines/exploration-build.yml @@ -28,9 +28,9 @@ steps: git config user.name "VSCode" git checkout origin/electron-11.x.y - git merge origin/master + git merge origin/main - # Push master branch into exploration branch + # Push main branch into exploration branch git push origin HEAD:electron-11.x.y displayName: Sync & Merge Exploration diff --git a/build/azure-pipelines/linux/product-build-alpine.yml b/build/azure-pipelines/linux/product-build-alpine.yml index a2bbb119bfb..4a1b8a2c64a 100644 --- a/build/azure-pipelines/linux/product-build-alpine.yml +++ b/build/azure-pipelines/linux/product-build-alpine.yml @@ -69,6 +69,7 @@ steps: displayName: Extract node_modules cache - script: | + set -e npx https://aka.ms/enablesecurefeed standAlone timeoutInMinutes: 5 condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), eq(variables['ENABLE_TERRAPIN'], 'true')) @@ -132,7 +133,3 @@ steps: artifact: vscode-server-linux-alpine-web displayName: Publish web server archive condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - - - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 - displayName: "Component Detection" - continueOnError: true diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index 9a42f058155..d0c8e0894e7 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -67,6 +67,7 @@ steps: condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), eq(variables['VSCODE_ARCH'], 'x64')) - script: | + set -e npx https://aka.ms/enablesecurefeed standAlone timeoutInMinutes: 5 condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), eq(variables['ENABLE_TERRAPIN'], 'true')) @@ -286,7 +287,3 @@ steps: artifactName: "snap-$(VSCODE_ARCH)" targetPath: .build/linux/snap-tarball condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - - - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 - displayName: "Component Detection" - continueOnError: true diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index 4d376246944..011f8d4f7d8 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -5,7 +5,7 @@ schedules: displayName: Mon-Fri at 7:00 branches: include: - - master + - main parameters: - name: VSCODE_QUALITY diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 08e3f694520..4f2024a6209 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -50,6 +50,7 @@ steps: displayName: Extract node_modules cache - script: | + set -e npx https://aka.ms/enablesecurefeed standAlone timeoutInMinutes: 5 condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), eq(variables['ENABLE_TERRAPIN'], 'true')) @@ -136,3 +137,7 @@ steps: targetPath: $(Build.ArtifactStagingDirectory)/compilation.tar.gz artifactName: Compilation displayName: Publish compilation artifact + + - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 + displayName: "Component Detection" + continueOnError: true diff --git a/build/azure-pipelines/publish-types/publish-types.yml b/build/azure-pipelines/publish-types/publish-types.yml index 0e3f4e4daa4..09964dc6ad0 100644 --- a/build/azure-pipelines/publish-types/publish-types.yml +++ b/build/azure-pipelines/publish-types/publish-types.yml @@ -61,7 +61,7 @@ steps: TAG_VERSION=$(git describe --tags `git rev-list --tags --max-count=1`) CHANNEL="G1C14HJ2F" - MESSAGE="DefinitelyTyped/DefinitelyTyped#vscode-types-$TAG_VERSION created. Endgame master, please open this link, examine changes and create a PR:" + MESSAGE="DefinitelyTyped/DefinitelyTyped#vscode-types-$TAG_VERSION created. Endgame champion, please open this link, examine changes and create a PR:" LINK="https://github.com/DefinitelyTyped/DefinitelyTyped/compare/vscode-types-$TAG_VERSION?quick_pull=1&body=Updating%20VS%20Code%20Extension%20API.%20See%20https%3A%2F%2Fgithub.com%2Fmicrosoft%2Fvscode%2Fissues%2F70175%20for%20details." MESSAGE2="[@eamodio, @jrieken, @kmaetzel, @egamma]. Please review and merge PR to publish @types/vscode." diff --git a/build/azure-pipelines/publish-types/update-types.ts b/build/azure-pipelines/publish-types/update-types.ts index bbce67221da..eae002e23a7 100644 --- a/build/azure-pipelines/publish-types/update-types.ts +++ b/build/azure-pipelines/publish-types/update-types.ts @@ -72,7 +72,7 @@ function getNewFileHeader(tag: string) { `/*---------------------------------------------------------------------------------------------`, ` * Copyright (c) Microsoft Corporation. All rights reserved.`, ` * Licensed under the MIT License.`, - ` * See https://github.com/microsoft/vscode/blob/master/LICENSE.txt for license information.`, + ` * See https://github.com/microsoft/vscode/blob/main/LICENSE.txt for license information.`, ` *--------------------------------------------------------------------------------------------*/`, ``, `/**`, diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index 05aa68fe126..0a8e1c36a88 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -60,6 +60,7 @@ steps: displayName: Extract node_modules cache - script: | + set -e npx https://aka.ms/enablesecurefeed standAlone timeoutInMinutes: 5 condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), eq(variables['ENABLE_TERRAPIN'], 'true')) diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 035580035b0..5ca1f825865 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -65,8 +65,10 @@ steps: condition: and(succeeded(), eq(variables.NODE_MODULES_RESTORED, 'true')) displayName: Extract node_modules cache - - script: | - npx https://aka.ms/enablesecurefeed standAlone + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { npx https://aka.ms/enablesecurefeed standAlone } timeoutInMinutes: 5 condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), eq(variables['ENABLE_TERRAPIN'], 'true')) displayName: Switch to Terrapin packages @@ -320,7 +322,3 @@ steps: artifact: vscode-server-win32-$(VSCODE_ARCH)-web displayName: Publish web server archive condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - - - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 - displayName: "Component Detection" - continueOnError: true diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index 7ef97f15be3..230082e4ad9 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -49,7 +49,7 @@ let BUNDLED_FILE_HEADER = [ ' * Copyright (c) Microsoft Corporation. All rights reserved.', ' * Version: ' + headerVersion, ' * Released under the MIT license', - ' * https://github.com/microsoft/vscode/blob/master/LICENSE.txt', + ' * https://github.com/microsoft/vscode/blob/main/LICENSE.txt', ' *-----------------------------------------------------------*/', '' ].join('\n'); diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 6d3a369082a..e3ca3514bb6 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -52,13 +52,13 @@ const vscodeResources = [ 'out-build/bootstrap-amd.js', 'out-build/bootstrap-node.js', 'out-build/bootstrap-window.js', - 'out-build/paths.js', 'out-build/vs/**/*.{svg,png,html,jpg}', '!out-build/vs/code/browser/**/*.html', '!out-build/vs/editor/standalone/**/*.svg', 'out-build/vs/base/common/performance.js', 'out-build/vs/base/node/languagePacks.js', 'out-build/vs/base/node/{stdForkStart.js,terminateProcess.sh,cpuUsage.sh,ps.sh}', + 'out-build/vs/base/node/userDataPath.js', 'out-build/vs/base/browser/ui/codicons/codicon/**', 'out-build/vs/base/parts/sandbox/electron-browser/preload.js', 'out-build/vs/workbench/browser/media/*-theme.css', @@ -284,6 +284,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op let result = all .pipe(util.skipDirectories()) .pipe(util.fixWin32DirectoryPermissions()) + .pipe(filter(['**', '!**/.github/**'], { dot: true })) // https://github.com/microsoft/vscode/issues/116523 .pipe(electron(_.extend({}, config, { platform, arch: arch === 'armhf' ? 'arm' : arch, ffmpegChromium: true }))) .pipe(filter(['**', '!LICENSE', '!LICENSES.chromium.html', '!version'], { dot: true })); @@ -526,7 +527,7 @@ gulp.task(task.define( if (!shouldSetupSettingsSearch()) { const branch = process.env.BUILD_SOURCEBRANCH; - console.log(`Only runs on master and release branches, not ${branch}`); + console.log(`Only runs on main and release branches, not ${branch}`); return; } @@ -552,21 +553,21 @@ gulp.task(task.define( function shouldSetupSettingsSearch() { const branch = process.env.BUILD_SOURCEBRANCH; - return branch && (/\/master$/.test(branch) || branch.indexOf('/release/') >= 0); + return branch && (/\/main$/.test(branch) || branch.indexOf('/release/') >= 0); } function getSettingsSearchBuildId(packageJson) { try { const branch = process.env.BUILD_SOURCEBRANCH; const branchId = branch.indexOf('/release/') >= 0 ? 0 : - /\/master$/.test(branch) ? 1 : + /\/main$/.test(branch) ? 1 : 2; // Some unexpected branch const out = cp.execSync(`git rev-list HEAD --count`); const count = parseInt(out.toString()); // - // 1.25.1, 1,234,567 commits, master = 1250112345671 + // 1.25.1, 1,234,567 commits, main = 1250112345671 return util.versionStringToNumber(packageJson.version) * 1e8 + count * 10 + branchId; } catch (e) { throw new Error('Could not determine build number: ' + e.toString()); diff --git a/build/monaco/README-npm.md b/build/monaco/README-npm.md index ee0ffc6e95c..737e06bbc5c 100644 --- a/build/monaco/README-npm.md +++ b/build/monaco/README-npm.md @@ -11,4 +11,4 @@ a good page describing the code editor's features is [here](https://code.visuals This npm module contains the core editor functionality, as it comes from the [vscode repository](https://github.com/microsoft/vscode). ## License -[MIT](https://github.com/microsoft/vscode/blob/master/LICENSE.txt) +[MIT](https://github.com/microsoft/vscode/blob/main/LICENSE.txt) diff --git a/extensions/emmet/package.json b/extensions/emmet/package.json index c786c715cde..0766cda7bc4 100644 --- a/extensions/emmet/package.json +++ b/extensions/emmet/package.json @@ -212,6 +212,11 @@ "type": "number", "default": 0.3, "description": "%emmetPreferencesCssFuzzySearchMinScore%" + }, + "output.reverseAttributes": { + "type": "boolean", + "default": false, + "description": "%emmetPreferencesOutputReverseAttributes%" } } }, @@ -430,8 +435,7 @@ "deps": "yarn add vscode-emmet-helper" }, "devDependencies": { - "@types/node": "^12.19.9", - "emmet": "https://github.com/rzhao271/emmet.git#1b2df677d8925ef5ea6da9df8845968403979a0a" + "@types/node": "^12.19.9" }, "dependencies": { "@emmetio/abbreviation": "^2.2.0", @@ -439,7 +443,7 @@ "@emmetio/html-matcher": "^0.3.3", "@emmetio/math-expression": "^1.0.4", "image-size": "^0.5.2", - "vscode-emmet-helper": "2.2.4", + "vscode-emmet-helper": "^2.3.0", "vscode-languageserver-textdocument": "^1.0.1" } } diff --git a/extensions/emmet/package.nls.json b/extensions/emmet/package.nls.json index 2a1add8935e..8e168ec4c4e 100644 --- a/extensions/emmet/package.nls.json +++ b/extensions/emmet/package.nls.json @@ -55,5 +55,6 @@ "emmetPreferencesCssOProperties": "Comma separated CSS properties that get the 'o' vendor prefix when used in Emmet abbreviation that starts with `-`. Set to empty string to always avoid the 'o' prefix.", "emmetPreferencesCssMsProperties": "Comma separated CSS properties that get the 'ms' vendor prefix when used in Emmet abbreviation that starts with `-`. Set to empty string to always avoid the 'ms' prefix.", "emmetPreferencesCssFuzzySearchMinScore": "The minimum score (from 0 to 1) that fuzzy-matched abbreviation should achieve. Lower values may produce many false-positive matches, higher values may reduce possible matches.", - "emmetOptimizeStylesheetParsing": "When set to `false`, the whole file is parsed to determine if current position is valid for expanding Emmet abbreviations. When set to `true`, only the content around the current position in css/scss/less files is parsed." + "emmetOptimizeStylesheetParsing": "When set to `false`, the whole file is parsed to determine if current position is valid for expanding Emmet abbreviations. When set to `true`, only the content around the current position in css/scss/less files is parsed.", + "emmetPreferencesOutputReverseAttributes": "If `true`, reverses attribute merging directions when resolving snippets." } diff --git a/extensions/emmet/yarn.lock b/extensions/emmet/yarn.lock index bebce02cf48..e9ac076c3f0 100644 --- a/extensions/emmet/yarn.lock +++ b/extensions/emmet/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@emmetio/abbreviation@^2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@emmetio/abbreviation/-/abbreviation-2.2.0.tgz#9f8dedbdb00e3136d6d37c6415375c82c0bb477f" - integrity sha512-NPGVUmnr7cLj4i6MKS4c8NjuoIIJROrruJl/8nXsp2MdbDRHvtfq25foySvv/NbfqTQm+P9JzVLDD9JxGIpvkQ== +"@emmetio/abbreviation@^2.2.0", "@emmetio/abbreviation@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@emmetio/abbreviation/-/abbreviation-2.2.1.tgz#d9458fe1f09fe042f019c48aa681165ba613a48d" + integrity sha512-uUNwNgbH0JPlrdXhy8VQbNPLLG7abMvOaLVMblx22i68Rl9r+2N235ALgIYFUty1yXC9DkVw6xMbz/D4QVARcQ== dependencies: "@emmetio/scanner" "^1.0.0" @@ -54,15 +54,16 @@ integrity sha1-Rs/+oRmgoAMxKiHC2bVijLX81EI= "@types/node@^12.19.9": - version "12.19.15" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.15.tgz#0de7e978fb43db62da369db18ea088a63673c182" - integrity sha512-lowukE3GUI+VSYSu6VcBXl14d61Rp5hA1D+61r16qnwC0lYNSqdxcvRh0pswejorHfS+HgwBasM8jLXz0/aOsw== + version "12.20.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.0.tgz#692dfdecd6c97f5380c42dd50f19261f9f604deb" + integrity sha512-0/41wHcurotvSOTHQUFkgL702c3pyWR1mToSrrX3pGPvGfpHTv3Ksx0M4UVuU5VJfjVb62Eyr1eKO1tWNUCg2Q== -"emmet@https://github.com/rzhao271/emmet.git#1b2df677d8925ef5ea6da9df8845968403979a0a": - version "2.3.0" - resolved "https://github.com/rzhao271/emmet.git#1b2df677d8925ef5ea6da9df8845968403979a0a" +emmet@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/emmet/-/emmet-2.3.1.tgz#77614d949d1d01e5c248d08043a13a7f4d539e47" + integrity sha512-u8h++9u3y9QWhn0imUXfQO+s80To5MGD97zd/00wGC39CfNGBPe//ZKepJz9I1LQ2FDRXHrn+e3JaN/53Y5z6A== dependencies: - "@emmetio/abbreviation" "^2.2.0" + "@emmetio/abbreviation" "^2.2.1" "@emmetio/css-abbreviation" "^2.1.2" image-size@^0.5.2: @@ -75,12 +76,12 @@ jsonc-parser@^2.3.0: resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.1.tgz#59549150b133f2efacca48fe9ce1ec0659af2342" integrity sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg== -vscode-emmet-helper@2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/vscode-emmet-helper/-/vscode-emmet-helper-2.2.4.tgz#8ab86d2b7fe9e6270b4c77c9fd8d1eb8f3f4c401" - integrity sha512-1N6bMzP1ZzkDGzamvsKxQ/lOmBc4+OQdj0dA2C9A5PSeYV9gh5xbJ061sm+VyFHOGZE+VyUQq5m/WFmFsLbKnA== +vscode-emmet-helper@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/vscode-emmet-helper/-/vscode-emmet-helper-2.3.0.tgz#a98357ad5ac9c71d7c00396f22b7963b1a74cc5c" + integrity sha512-QhU8+HlynMuUkqBYgA3wIDTSsUNkw8GWxLR214Hjvwr0lkFZa0CRqW/PzAI1CFREjSrTxJYQvkVnbfatZzKAuA== dependencies: - emmet "https://github.com/rzhao271/emmet.git#1b2df677d8925ef5ea6da9df8845968403979a0a" + emmet "^2.3.0" jsonc-parser "^2.3.0" vscode-languageserver-textdocument "^1.0.1" vscode-languageserver-types "^3.15.1" diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index 0fe917632fe..726949d02e0 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -53,7 +53,7 @@ "vscode:prepublish": "npm run compile" }, "dependencies": { - "node-fetch": "2.6.0", + "node-fetch": "2.6.1", "uuid": "8.1.0", "vscode-extension-telemetry": "0.1.1", "vscode-nls": "^4.1.2" diff --git a/extensions/github-authentication/src/extension.ts b/extensions/github-authentication/src/extension.ts index c9b76d81670..7ae1e1d2110 100644 --- a/extensions/github-authentication/src/extension.ts +++ b/extensions/github-authentication/src/extension.ts @@ -14,7 +14,7 @@ export async function activate(context: vscode.ExtensionContext) { const telemetryReporter = new TelemetryReporter(name, version, aiKey); context.subscriptions.push(vscode.window.registerUriHandler(uriHandler)); - const loginService = new GitHubAuthenticationProvider(context); + const loginService = new GitHubAuthenticationProvider(context, telemetryReporter); await loginService.initialize(context); @@ -24,8 +24,7 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.authentication.registerAuthenticationProvider('github', 'GitHub', { onDidChangeSessions: onDidChangeSessions.event, - getAllSessions: () => Promise.resolve(loginService.sessions), - getSessions: (scopes: string[]) => loginService.getSessions(scopes), + getSessions: (scopes?: string[]) => loginService.getSessions(scopes), createSession: async (scopeList: string[]) => { try { /* __GDPR__ diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index 89893c94ccd..595d13147a1 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -9,6 +9,7 @@ import { Keychain } from './common/keychain'; import { GitHubServer, NETWORK_ERROR } from './githubServer'; import Logger from './common/logger'; import { arrayEquals } from './common/utils'; +import TelemetryReporter from 'vscode-extension-telemetry'; export const onDidChangeSessions = new vscode.EventEmitter(); @@ -25,12 +26,13 @@ interface SessionData { export class GitHubAuthenticationProvider { private _sessions: vscode.AuthenticationSession[] = []; - private _githubServer = new GitHubServer(); + private _githubServer: GitHubServer; private _keychain: Keychain; - constructor(context: vscode.ExtensionContext) { + constructor(context: vscode.ExtensionContext, telemetryReporter: TelemetryReporter) { this._keychain = new Keychain(context); + this._githubServer = new GitHubServer(telemetryReporter); } public async initialize(context: vscode.ExtensionContext): Promise { @@ -44,8 +46,10 @@ export class GitHubAuthenticationProvider { context.subscriptions.push(context.secrets.onDidChange(() => this.checkForUpdates())); } - async getSessions(scopes: string[]): Promise { - return this._sessions.filter(session => arrayEquals(session.scopes, scopes)); + async getSessions(scopes?: string[]): Promise { + return scopes + ? this._sessions.filter(session => arrayEquals(session.scopes, scopes)) + : this._sessions; } private async verifySessions(): Promise { @@ -53,6 +57,7 @@ export class GitHubAuthenticationProvider { const verificationPromises = this._sessions.map(async session => { try { await this._githubServer.getUserInfo(session.accessToken); + this._githubServer.checkIsEdu(session.accessToken); verifiedSessions.push(session); } catch (e) { // Remove sessions that return unauthorized response @@ -161,6 +166,7 @@ export class GitHubAuthenticationProvider { public async createSession(scopes: string): Promise { const token = await this._githubServer.login(scopes); const session = await this.tokenToSession(token, scopes.split(' ')); + this._githubServer.checkIsEdu(token); await this.setToken(session); return session; } diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index b50285c9ab2..13ec13b735b 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -9,6 +9,7 @@ import fetch, { Response } from 'node-fetch'; import { v4 as uuid } from 'uuid'; import { PromiseAdapter, promiseFromEvent } from './common/utils'; import Logger from './common/logger'; +import TelemetryReporter from 'vscode-extension-telemetry'; const localize = nls.loadMessageBundle(); @@ -41,6 +42,8 @@ export class GitHubServer { private _pendingStates = new Map(); private _codeExchangePromises = new Map>(); + constructor(private readonly telemetryReporter: TelemetryReporter) { } + private isTestEnvironment(url: vscode.Uri): boolean { return url.authority === 'vscode-web-test-playground.azurewebsites.net' || url.authority.startsWith('localhost:'); } @@ -210,4 +213,36 @@ export class GitHubServer { throw new Error(result.statusText); } } + + public async checkIsEdu(token: string): Promise { + try { + const result = await fetch('https://education.github.com/api/user', { + headers: { + Authorization: `token ${token}`, + 'faculty-check-preview': 'true', + 'User-Agent': 'Visual-Studio-Code' + } + }); + + if (result.ok) { + const json: { student: boolean, faculty: boolean } = await result.json(); + + /* __GDPR__ + "session" : { + "isEdu": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } + } + */ + this.telemetryReporter.sendTelemetryEvent('session', { + isEdu: json.student + ? 'student' + : json.faculty + ? 'faculty' + : 'none' + }); + } + } catch (e) { + // No-op + } + + } } diff --git a/extensions/github-authentication/yarn.lock b/extensions/github-authentication/yarn.lock index 1da6beed526..5220f268c51 100644 --- a/extensions/github-authentication/yarn.lock +++ b/extensions/github-authentication/yarn.lock @@ -84,10 +84,10 @@ mime-types@^2.1.12: dependencies: mime-db "1.44.0" -node-fetch@2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" - integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== +node-fetch@2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== semver@^5.3.0: version "5.7.1" diff --git a/extensions/github/yarn.lock b/extensions/github/yarn.lock index 5e790ac2903..05a0b4cf6f9 100644 --- a/extensions/github/yarn.lock +++ b/extensions/github/yarn.lock @@ -125,9 +125,9 @@ is-plain-object@^3.0.0: integrity sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g== 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== + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== once@^1.4.0: version "1.4.0" diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index e6416410be3..54405b6939f 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -47,7 +47,7 @@ }, "dependencies": { "buffer": "^5.6.0", - "node-fetch": "^2.6.0", + "node-fetch": "2.6.1", "randombytes": "github:rmacfarlane/randombytes#b28d4ecee46262801ea09f15fa1f1513a05c5971", "sha.js": "2.4.11", "stream": "0.0.2", diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index fbb4e10a0e8..b0631344346 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -300,7 +300,11 @@ export class AzureActiveDirectoryService { return Promise.all(this._tokens.map(token => this.convertToSession(token))); } - async getSessions(scopes: string[]): Promise { + async getSessions(scopes?: string[]): Promise { + if (!scopes) { + return this.sessions; + } + const orderedScopes = scopes.sort().join(' '); const matchingTokens = this._tokens.filter(token => token.scope === orderedScopes); return Promise.all(matchingTokens.map(token => this.convertToSession(token))); diff --git a/extensions/microsoft-authentication/src/extension.ts b/extensions/microsoft-authentication/src/extension.ts index 584a4027b64..41b8690f344 100644 --- a/extensions/microsoft-authentication/src/extension.ts +++ b/extensions/microsoft-authentication/src/extension.ts @@ -20,7 +20,6 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.authentication.registerAuthenticationProvider('microsoft', 'Microsoft', { onDidChangeSessions: onDidChangeSessions.event, - getAllSessions: () => Promise.resolve(loginService.sessions), getSessions: (scopes: string[]) => loginService.getSessions(scopes), createSession: async (scopes: string[]) => { try { diff --git a/extensions/microsoft-authentication/yarn.lock b/extensions/microsoft-authentication/yarn.lock index 58e34ee843a..b58b488d970 100644 --- a/extensions/microsoft-authentication/yarn.lock +++ b/extensions/microsoft-authentication/yarn.lock @@ -126,10 +126,10 @@ mime-types@^2.1.12: dependencies: mime-db "1.44.0" -node-fetch@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" - integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== +node-fetch@2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== "randombytes@github:rmacfarlane/randombytes#b28d4ecee46262801ea09f15fa1f1513a05c5971": version "2.1.0" diff --git a/extensions/theme-abyss/themes/abyss-color-theme.json b/extensions/theme-abyss/themes/abyss-color-theme.json index 8e97bbb6dc1..391b5af61b7 100644 --- a/extensions/theme-abyss/themes/abyss-color-theme.json +++ b/extensions/theme-abyss/themes/abyss-color-theme.json @@ -303,7 +303,7 @@ "list.activeSelectionBackground": "#08286b", // "list.activeSelectionForeground": "", - "list.focusBackground": "#08286b", + "quickInput.list.focusBackground": "#08286b", "list.hoverBackground": "#061940", "list.inactiveSelectionBackground": "#152037", "list.dropBackground": "#041D52", diff --git a/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json b/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json index 3453c53dc2b..24ba31854fe 100644 --- a/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json +++ b/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json @@ -10,7 +10,7 @@ "list.highlightForeground": "#e3b583", "list.activeSelectionBackground": "#7c5021", "list.hoverBackground": "#7c502166", - "list.focusBackground": "#7c5021AA", + "quickInput.list.focusBackground": "#7c5021AA", "list.inactiveSelectionBackground": "#645342", "pickerGroup.foreground": "#e3b583", "pickerGroup.border": "#e3b583", diff --git a/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json b/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json index bd8b896c011..9ca62cc15e3 100644 --- a/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json +++ b/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json @@ -3,7 +3,7 @@ "colors": { "dropdown.background": "#525252", "list.activeSelectionBackground": "#707070", - "list.focusBackground": "#707070", + "quickInput.list.focusBackground": "#707070", "list.inactiveSelectionBackground": "#4e4e4e", "list.hoverBackground": "#444444", "list.highlightForeground": "#e58520", diff --git a/extensions/theme-monokai/themes/monokai-color-theme.json b/extensions/theme-monokai/themes/monokai-color-theme.json index 969455d01e3..100d291ddad 100644 --- a/extensions/theme-monokai/themes/monokai-color-theme.json +++ b/extensions/theme-monokai/themes/monokai-color-theme.json @@ -9,7 +9,7 @@ "colors": { "dropdown.background": "#414339", "list.activeSelectionBackground": "#75715E", - "list.focusBackground": "#414339", + "quickInput.list.focusBackground": "#414339", "dropdown.listBackground": "#1e1f1c", "list.inactiveSelectionBackground": "#414339", "list.hoverBackground": "#3e3d32", diff --git a/extensions/theme-quietlight/themes/quietlight-color-theme.json b/extensions/theme-quietlight/themes/quietlight-color-theme.json index e395b99e3eb..376c5fda7a3 100644 --- a/extensions/theme-quietlight/themes/quietlight-color-theme.json +++ b/extensions/theme-quietlight/themes/quietlight-color-theme.json @@ -473,7 +473,7 @@ "pickerGroup.foreground": "#A6B39B", "pickerGroup.border": "#749351", "list.activeSelectionForeground": "#6c6c6c", - "list.focusBackground": "#CADEB9", + "quickInput.list.focusBackground": "#CADEB9", "list.hoverBackground": "#e0e0e0", "list.activeSelectionBackground": "#c4d9b1", "list.inactiveSelectionBackground": "#d3dbcd", diff --git a/extensions/theme-red/themes/Red-color-theme.json b/extensions/theme-red/themes/Red-color-theme.json index 8eeda13456e..c3e728b7f39 100644 --- a/extensions/theme-red/themes/Red-color-theme.json +++ b/extensions/theme-red/themes/Red-color-theme.json @@ -49,7 +49,7 @@ "list.activeSelectionBackground": "#880000", "list.inactiveSelectionBackground": "#770000", "list.dropBackground": "#662222", - "list.focusBackground": "#660000", + "quickInput.list.focusBackground": "#660000", "list.highlightForeground": "#ff4444", "pickerGroup.foreground": "#cc9999", "pickerGroup.border": "#ff000033", diff --git a/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json b/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json index 8a9deb0cd4b..b887521b605 100644 --- a/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json +++ b/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json @@ -350,7 +350,7 @@ "list.activeSelectionBackground": "#005A6F", // "list.activeSelectionForeground": "", - "list.focusBackground": "#005A6F", + "quickInput.list.focusBackground": "#005A6F", "list.hoverBackground": "#004454AA", "list.inactiveSelectionBackground": "#00445488", "list.dropBackground": "#00445488", diff --git a/extensions/theme-solarized-light/themes/solarized-light-color-theme.json b/extensions/theme-solarized-light/themes/solarized-light-color-theme.json index 427b7c3c359..1efe1e000e3 100644 --- a/extensions/theme-solarized-light/themes/solarized-light-color-theme.json +++ b/extensions/theme-solarized-light/themes/solarized-light-color-theme.json @@ -350,7 +350,7 @@ "list.activeSelectionBackground": "#DFCA88", "list.activeSelectionForeground": "#6C6C6C", - "list.focusBackground": "#DFCA8866", + "quickInput.list.focusBackground": "#DFCA8866", "list.hoverBackground": "#DFCA8844", "list.inactiveSelectionBackground": "#D1CBB8", "list.highlightForeground": "#B58900", diff --git a/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-color-theme.json b/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-color-theme.json index 91c135342f1..e4ae768d9f5 100644 --- a/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-color-theme.json +++ b/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-color-theme.json @@ -5,7 +5,7 @@ "errorForeground": "#a92049", "input.background": "#001733", "dropdown.background": "#001733", - "list.focusBackground": "#ffffff60", + "quickInput.list.focusBackground": "#ffffff60", "list.activeSelectionBackground": "#ffffff60", "list.inactiveSelectionBackground": "#ffffff40", "list.hoverBackground": "#ffffff30", diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 228a9681278..7e576805d5c 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -116,6 +116,27 @@ } ] } + ], + "notebookProvider": [ + { + "viewType": "notebookCoreTest", + "displayName": "Notebook Core Test", + "selector": [ + { + "filenamePattern": "*.vsctestnb", + "excludeFileNamePattern": "" + } + ] + }, + { + "viewType": "notebook.nbdtest", + "displayName": "notebook.nbdtest", + "selector": [ + { + "filenamePattern": "**/*.nbdtest" + } + ] + } ] }, "scripts": { diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts new file mode 100644 index 00000000000..34a8218e3a1 --- /dev/null +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts @@ -0,0 +1,235 @@ +/*--------------------------------------------------------------------------------------------- + * 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 vscode from 'vscode'; +import * as utils from '../utils'; + +suite('Notebook Document', function () { + + const contentProvider = new class implements vscode.NotebookContentProvider { + async openNotebook(uri: vscode.Uri, _openContext: vscode.NotebookDocumentOpenContext): Promise { + return { + cells: [{ cellKind: vscode.NotebookCellKind.Code, source: uri.toString(), language: 'javascript', metadata: {}, outputs: [] }], + metadata: {} + }; + } + async resolveNotebook(_document: vscode.NotebookDocument, _webview: vscode.NotebookCommunication) { + // + } + async saveNotebook(_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) { + // + } + async saveNotebookAs(_targetResource: vscode.Uri, _document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) { + // + } + async backupNotebook(_document: vscode.NotebookDocument, _context: vscode.NotebookDocumentBackupContext, _cancellation: vscode.CancellationToken) { + return { id: '', delete() { } }; + } + }; + + const disposables: vscode.Disposable[] = []; + + suiteTeardown(async function () { + // utils.assertNoRpc(); + await utils.revertAllDirty(); + await utils.closeAllEditors(); + utils.disposeAll(disposables); + disposables.length = 0; + + for (let doc of vscode.notebook.notebookDocuments) { + assert.strictEqual(doc.isDirty, false, doc.uri.toString()); + } + }); + + suiteSetup(function () { + disposables.push(vscode.notebook.registerNotebookContentProvider('notebook.nbdtest', contentProvider)); + }); + + test('cannot register sample provider multiple times', function () { + assert.throws(() => { + vscode.notebook.registerNotebookContentProvider('notebook.nbdtest', contentProvider); + }); + }); + + test('cannot open unknown types', async function () { + try { + await vscode.notebook.openNotebookDocument(vscode.Uri.parse('some:///thing.notTypeKnown')); + assert.ok(false); + } catch { + assert.ok(true); + } + }); + + test('document basics', async function () { + const uri = await utils.createRandomFile(undefined, undefined, '.nbdtest'); + const notebook = await vscode.notebook.openNotebookDocument(uri); + + assert.strictEqual(notebook.uri.toString(), uri.toString()); + assert.strictEqual(notebook.isDirty, false); + assert.strictEqual(notebook.isUntitled, false); + assert.strictEqual(notebook.cells.length, 1); + + assert.strictEqual(notebook.viewType, 'notebook.nbdtest'); + }); + + test('notebook open/close, notebook ready when cell-document open event is fired', async function () { + const uri = await utils.createRandomFile(undefined, undefined, '.nbdtest'); + let didHappen = false; + const p = utils.asPromise(vscode.workspace.onDidOpenTextDocument).then(doc => { + if (doc.uri.scheme !== 'vscode-notebook-cell') { + return; + } + const notebook = vscode.notebook.notebookDocuments.find(notebook => { + const cell = notebook.cells.find(cell => cell.document === doc); + return Boolean(cell); + }); + assert.ok(notebook, `notebook for cell ${doc.uri} NOT found`); + didHappen = true; + }); + + await vscode.notebook.openNotebookDocument(uri); + await p; + assert.strictEqual(didHappen, true); + }); + + test('notebook open/close, all cell-documents are ready', async function () { + const uri = await utils.createRandomFile(undefined, undefined, '.nbdtest'); + + const p = utils.asPromise(vscode.notebook.onDidOpenNotebookDocument).then(notebook => { + for (let cell of notebook.cells) { + const doc = vscode.workspace.textDocuments.find(doc => doc.uri.toString() === cell.uri.toString()); + assert.ok(doc); + assert.strictEqual(doc.notebook === notebook, true); + assert.strictEqual(doc === cell.document, true); + assert.strictEqual(doc?.languageId, cell.language); + assert.strictEqual(doc?.isDirty, false); + assert.strictEqual(doc?.isClosed, false); + } + }); + + await vscode.notebook.openNotebookDocument(uri); + await p; + }); + + + test('workspace edit API (replaceCells)', async function () { + const uri = await utils.createRandomFile(undefined, undefined, '.nbdtest'); + + const document = await vscode.notebook.openNotebookDocument(uri); + assert.strictEqual(document.cells.length, 1); + + // inserting two new cells + { + const edit = new vscode.WorkspaceEdit(); + edit.replaceNotebookCells(document.uri, 0, 0, [{ + cellKind: vscode.NotebookCellKind.Markdown, + language: 'markdown', + metadata: undefined, + outputs: [], + source: 'new_markdown' + }, { + cellKind: vscode.NotebookCellKind.Code, + language: 'fooLang', + metadata: undefined, + outputs: [], + source: 'new_code' + }]); + + const success = await vscode.workspace.applyEdit(edit); + assert.strictEqual(success, true); + } + + assert.strictEqual(document.cells.length, 3); + assert.strictEqual(document.cells[0].document.getText(), 'new_markdown'); + assert.strictEqual(document.cells[1].document.getText(), 'new_code'); + + // deleting cell 1 and 3 + { + const edit = new vscode.WorkspaceEdit(); + edit.replaceNotebookCells(document.uri, 0, 1, []); + edit.replaceNotebookCells(document.uri, 2, 3, []); + const success = await vscode.workspace.applyEdit(edit); + assert.strictEqual(success, true); + } + + assert.strictEqual(document.cells.length, 1); + assert.strictEqual(document.cells[0].document.getText(), 'new_code'); + + // replacing all cells + { + const edit = new vscode.WorkspaceEdit(); + edit.replaceNotebookCells(document.uri, 0, 1, [{ + cellKind: vscode.NotebookCellKind.Markdown, + language: 'markdown', + metadata: undefined, + outputs: [], + source: 'new2_markdown' + }, { + cellKind: vscode.NotebookCellKind.Code, + language: 'fooLang', + metadata: undefined, + outputs: [], + source: 'new2_code' + }]); + const success = await vscode.workspace.applyEdit(edit); + assert.strictEqual(success, true); + } + assert.strictEqual(document.cells.length, 2); + assert.strictEqual(document.cells[0].document.getText(), 'new2_markdown'); + assert.strictEqual(document.cells[1].document.getText(), 'new2_code'); + + // remove all cells + { + const edit = new vscode.WorkspaceEdit(); + edit.replaceNotebookCells(document.uri, 0, document.cells.length, []); + const success = await vscode.workspace.applyEdit(edit); + assert.strictEqual(success, true); + } + assert.strictEqual(document.cells.length, 0); + }); + + test('workspace edit API (replaceCells, event)', async function () { + const uri = await utils.createRandomFile(undefined, undefined, '.nbdtest'); + const document = await vscode.notebook.openNotebookDocument(uri); + assert.strictEqual(document.cells.length, 1); + + const edit = new vscode.WorkspaceEdit(); + edit.replaceNotebookCells(document.uri, 0, 0, [{ + cellKind: vscode.NotebookCellKind.Markdown, + language: 'markdown', + metadata: undefined, + outputs: [], + source: 'new_markdown' + }, { + cellKind: vscode.NotebookCellKind.Code, + language: 'fooLang', + metadata: undefined, + outputs: [], + source: 'new_code' + }]); + + const event = utils.asPromise(vscode.notebook.onDidChangeNotebookCells); + + const success = await vscode.workspace.applyEdit(edit); + assert.strictEqual(success, true); + + const data = await event; + + // check document + assert.strictEqual(document.cells.length, 3); + assert.strictEqual(document.cells[0].document.getText(), 'new_markdown'); + assert.strictEqual(document.cells[1].document.getText(), 'new_code'); + + // check event data + assert.strictEqual(data.document === document, true); + assert.strictEqual(data.changes.length, 1); + assert.strictEqual(data.changes[0].deletedCount, 0); + assert.strictEqual(data.changes[0].deletedItems.length, 0); + assert.strictEqual(data.changes[0].items.length, 2); + assert.strictEqual(data.changes[0].items[0], document.cells[0]); + assert.strictEqual(data.changes[0].items[1], document.cells[1]); + }); +}); diff --git a/extensions/vscode-notebook-tests/src/notebook.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts similarity index 81% rename from extensions/vscode-notebook-tests/src/notebook.test.ts rename to extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts index 0117e41160a..b437120c23c 100644 --- a/extensions/vscode-notebook-tests/src/notebook.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts @@ -6,52 +6,13 @@ import 'mocha'; import * as assert from 'assert'; import * as vscode from 'vscode'; -import { createRandomFile } from './utils'; - -export function timeoutAsync(n: number): Promise { - return new Promise(resolve => { - setTimeout(() => { - resolve(); - }, n); - }); -} - -export function once(event: vscode.Event): vscode.Event { - return (listener: any, thisArgs = null, disposables?: any) => { - // we need this, in case the event fires during the listener call - let didFire = false; - let result: vscode.Disposable; - result = event(e => { - if (didFire) { - return; - } else if (result) { - result.dispose(); - } else { - didFire = true; - } - - return listener.call(thisArgs, e); - }, null, disposables); - - if (didFire) { - result.dispose(); - } - - return result; - }; -} - -async function getEventOncePromise(event: vscode.Event): Promise { - return new Promise((resolve, _reject) => { - once(event)((result: T) => resolve(result)); - }); -} +import { createRandomFile, asPromise, disposeAll, closeAllEditors, revertAllDirty } from '../utils'; // Since `workbench.action.splitEditor` command does await properly // Notebook editor/document events are not guaranteed to be sent to the ext host when promise resolves // The workaround here is waiting for the first visible notebook editor change event. async function splitEditor() { - const once = getEventOncePromise(vscode.window.onDidChangeVisibleNotebookEditors); + const once = asPromise(vscode.window.onDidChangeVisibleNotebookEditors); await vscode.commands.executeCommand('workbench.action.splitEditor'); await once; } @@ -100,7 +61,7 @@ async function updateNotebookMetadata(uri: vscode.Uri, newMetadata: vscode.Noteb } async function withEvent(event: vscode.Event, callback: (e: Promise) => Promise) { - const e = getEventOncePromise(event); + const e = asPromise(event); await callback(e); } @@ -112,7 +73,146 @@ function assertInitalState() { // assert.strictEqual(vscode.notebook.visibleNotebookEditors.length, 0); } -suite('Notebook API tests', () => { +suite('Notebook API tests', function () { + + const disposables: vscode.Disposable[] = []; + + suiteTeardown(async function () { + await revertAllDirty(); + await closeAllEditors(); + + disposeAll(disposables); + disposables.length = 0; + }); + + suiteSetup(function () { + disposables.push(vscode.notebook.registerNotebookContentProvider('notebookCoreTest', { + openNotebook: async (_resource: vscode.Uri): Promise => { + if (/.*empty\-.*\.vsctestnb$/.test(_resource.path)) { + return { + metadata: {}, + cells: [] + }; + } + + const dto: vscode.NotebookData = { + metadata: { + custom: { testMetadata: false } + }, + cells: [ + { + source: 'test', + language: 'typescript', + cellKind: vscode.NotebookCellKind.Code, + outputs: [], + metadata: { + custom: { testCellMetadata: 123 } + } + } + ] + }; + return dto; + }, + resolveNotebook: async (_document: vscode.NotebookDocument) => { + return; + }, + saveNotebook: async (_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { + return; + }, + saveNotebookAs: async (_targetResource: vscode.Uri, _document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { + return; + }, + backupNotebook: async (_document: vscode.NotebookDocument, _context: vscode.NotebookDocumentBackupContext, _cancellation: vscode.CancellationToken) => { + return { + id: '1', + delete: () => { } + }; + } + })); + + + const kernel: vscode.NotebookKernel = { + id: 'mainKernel', + label: 'Notebook Test Kernel', + isPreferred: true, + supportedLanguages: ['typescript'], + executeAllCells: async (_document: vscode.NotebookDocument) => { + const edit = new vscode.WorkspaceEdit(); + + edit.replaceNotebookCellOutput(_document.uri, 0, [new vscode.NotebookCellOutput([ + new vscode.NotebookCellOutputItem('text/plain', ['my output'], undefined) + ])]); + return vscode.workspace.applyEdit(edit); + }, + cancelAllCellsExecution: async (_document: vscode.NotebookDocument) => { }, + executeCell: async (document: vscode.NotebookDocument, cell: vscode.NotebookCell | undefined) => { + if (!cell) { + cell = document.cells[0]; + } + + if (document.uri.path.endsWith('customRenderer.vsctestnb')) { + const edit = new vscode.WorkspaceEdit(); + edit.replaceNotebookCellOutput(document.uri, cell.index, [new vscode.NotebookCellOutput([ + new vscode.NotebookCellOutputItem('text/custom', ['test'], undefined) + ])]); + + return vscode.workspace.applyEdit(edit); + } + + const edit = new vscode.WorkspaceEdit(); + // const previousOutputs = cell.outputs; + edit.replaceNotebookCellOutput(document.uri, cell.index, [new vscode.NotebookCellOutput([ + new vscode.NotebookCellOutputItem('text/plain', ['my output'], undefined) + ])]); + + return vscode.workspace.applyEdit(edit); + }, + cancelCellExecution: async (_document: vscode.NotebookDocument, _cell: vscode.NotebookCell) => { } + }; + + const kernel2: vscode.NotebookKernel = { + id: 'secondaryKernel', + label: 'Notebook Secondary Test Kernel', + isPreferred: false, + supportedLanguages: ['typescript'], + executeAllCells: async (_document: vscode.NotebookDocument) => { + const edit = new vscode.WorkspaceEdit(); + edit.replaceNotebookCellOutput(_document.uri, 0, [new vscode.NotebookCellOutput([ + new vscode.NotebookCellOutputItem('text/plain', ['my second output'], undefined) + ])]); + + return vscode.workspace.applyEdit(edit); + }, + cancelAllCellsExecution: async (_document: vscode.NotebookDocument) => { }, + executeCell: async (document: vscode.NotebookDocument, cell: vscode.NotebookCell | undefined) => { + if (!cell) { + cell = document.cells[0]; + } + + const edit = new vscode.WorkspaceEdit(); + + if (document.uri.path.endsWith('customRenderer.vsctestnb')) { + edit.replaceNotebookCellOutput(document.uri, cell.index, [new vscode.NotebookCellOutput([ + new vscode.NotebookCellOutputItem('text/custom', ['test 2'], undefined) + ])]); + } else { + edit.replaceNotebookCellOutput(document.uri, cell.index, [new vscode.NotebookCellOutput([ + new vscode.NotebookCellOutputItem('text/plain', ['my second output'], undefined) + ])]); + } + + return vscode.workspace.applyEdit(edit); + }, + cancelCellExecution: async (_document: vscode.NotebookDocument, _cell: vscode.NotebookCell) => { } + }; + + disposables.push(vscode.notebook.registerNotebookKernelProvider({ filenamePattern: '*.vsctestnb' }, { + provideKernels: async () => { + return [kernel, kernel2]; + } + })); + }); + // test.only('crash', async function () { // for (let i = 0; i < 200; i++) { // let resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); @@ -141,60 +241,20 @@ suite('Notebook API tests', () => { test('document open/close event', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); - const firstDocumentOpen = getEventOncePromise(vscode.notebook.onDidOpenNotebookDocument); + const resource = await createRandomFile('', undefined, '.vsctestnb'); + const firstDocumentOpen = asPromise(vscode.notebook.onDidOpenNotebookDocument); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await firstDocumentOpen; - const firstDocumentClose = getEventOncePromise(vscode.notebook.onDidCloseNotebookDocument); + const firstDocumentClose = asPromise(vscode.notebook.onDidCloseNotebookDocument); await vscode.commands.executeCommand('workbench.action.closeAllEditors'); await firstDocumentClose; }); - test('notebook open/close, all cell-documents are ready', async function () { - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); - - const p = getEventOncePromise(vscode.notebook.onDidOpenNotebookDocument).then(notebook => { - for (let cell of notebook.cells) { - const doc = vscode.workspace.textDocuments.find(doc => doc.uri.toString() === cell.uri.toString()); - assert.ok(doc); - assert.strictEqual(doc === cell.document, true); - assert.strictEqual(doc?.languageId, cell.language); - assert.strictEqual(doc?.isDirty, false); - assert.strictEqual(doc?.isClosed, false); - } - }); - - await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - await p; - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); - }); - - test('notebook open/close, notebook ready when cell-document open event is fired', async function () { - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); - let didHappen = false; - const p = getEventOncePromise(vscode.workspace.onDidOpenTextDocument).then(doc => { - if (doc.uri.scheme !== 'vscode-notebook-cell') { - return; - } - const notebook = vscode.notebook.notebookDocuments.find(notebook => { - const cell = notebook.cells.find(cell => cell.document === doc); - return Boolean(cell); - }); - assert.ok(notebook, `notebook for cell ${doc.uri} NOT found`); - didHappen = true; - }); - - await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - await p; - assert.strictEqual(didHappen, true); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); - }); - test('shared document in notebook editors', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); let counter = 0; const disposables: vscode.Disposable[] = []; disposables.push(vscode.notebook.onDidOpenNotebookDocument(() => { @@ -217,12 +277,12 @@ suite('Notebook API tests', () => { test('editor open/close event', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); - const firstEditorOpen = getEventOncePromise(vscode.window.onDidChangeVisibleNotebookEditors); + const resource = await createRandomFile('', undefined, '.vsctestnb'); + const firstEditorOpen = asPromise(vscode.window.onDidChangeVisibleNotebookEditors); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await firstEditorOpen; - const firstEditorClose = getEventOncePromise(vscode.window.onDidChangeVisibleNotebookEditors); + const firstEditorClose = asPromise(vscode.window.onDidChangeVisibleNotebookEditors); await vscode.commands.executeCommand('workbench.action.closeAllEditors'); await firstEditorClose; }); @@ -230,7 +290,7 @@ suite('Notebook API tests', () => { test('editor open/close event 2', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); let count = 0; const disposables: vscode.Disposable[] = []; disposables.push(vscode.window.onDidChangeVisibleNotebookEditors(() => { @@ -250,10 +310,10 @@ suite('Notebook API tests', () => { test('editor editing event 2', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - const cellsChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); + const cellsChangeEvent = asPromise(vscode.notebook.onDidChangeNotebookCells); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); const cellChangeEventRet = await cellsChangeEvent; assert.strictEqual(cellChangeEventRet.document, vscode.window.activeNotebookEditor?.document); @@ -269,7 +329,7 @@ suite('Notebook API tests', () => { const secondCell = vscode.window.activeNotebookEditor!.document.cells[1]; - const moveCellEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); + const moveCellEvent = asPromise(vscode.notebook.onDidChangeNotebookCells); await vscode.commands.executeCommand('notebook.cell.moveUp'); const moveCellEventRet = await moveCellEvent; assert.deepStrictEqual(moveCellEventRet, { @@ -290,7 +350,7 @@ suite('Notebook API tests', () => { ] }); - const cellOutputChange = getEventOncePromise(vscode.notebook.onDidChangeCellOutputs); + const cellOutputChange = asPromise(vscode.notebook.onDidChangeCellOutputs); await vscode.commands.executeCommand('notebook.cell.execute'); const cellOutputsAddedRet = await cellOutputChange; assert.deepStrictEqual(cellOutputsAddedRet, { @@ -299,7 +359,7 @@ suite('Notebook API tests', () => { }); assert.strictEqual(cellOutputsAddedRet.cells[0].outputs.length, 1); - const cellOutputClear = getEventOncePromise(vscode.notebook.onDidChangeCellOutputs); + const cellOutputClear = asPromise(vscode.notebook.onDidChangeCellOutputs); await vscode.commands.executeCommand('notebook.cell.clearOutputs'); const cellOutputsCleardRet = await cellOutputClear; assert.deepStrictEqual(cellOutputsCleardRet, { @@ -323,7 +383,7 @@ suite('Notebook API tests', () => { test('editor move cell event', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); @@ -331,7 +391,7 @@ suite('Notebook API tests', () => { const activeCell = vscode.window.activeNotebookEditor!.selection; assert.strictEqual(vscode.window.activeNotebookEditor!.document.cells.indexOf(activeCell!), 0); - const moveChange = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); + const moveChange = asPromise(vscode.notebook.onDidChangeNotebookCells); await vscode.commands.executeCommand('notebook.cell.moveDown'); const ret = await moveChange; assert.deepStrictEqual(ret, { @@ -365,7 +425,7 @@ suite('Notebook API tests', () => { test('notebook editor active/visible', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); const firstEditor = vscode.window.activeNotebookEditor; assert.strictEqual(firstEditor && vscode.window.visibleNotebookEditors.indexOf(firstEditor) >= 0, true); @@ -377,7 +437,7 @@ suite('Notebook API tests', () => { assert.strictEqual(firstEditor && vscode.window.visibleNotebookEditors.indexOf(firstEditor) >= 0, true); assert.strictEqual(vscode.window.visibleNotebookEditors.length, 2); - const untitledEditorChange = getEventOncePromise(vscode.window.onDidChangeActiveNotebookEditor); + const untitledEditorChange = asPromise(vscode.window.onDidChangeActiveNotebookEditor); await vscode.commands.executeCommand('workbench.action.files.newUntitledFile'); await untitledEditorChange; assert.strictEqual(firstEditor && vscode.window.visibleNotebookEditors.indexOf(firstEditor) >= 0, true); @@ -386,7 +446,7 @@ suite('Notebook API tests', () => { assert.notStrictEqual(secondEditor, vscode.window.activeNotebookEditor); assert.strictEqual(vscode.window.visibleNotebookEditors.length, 1); - const activeEditorClose = getEventOncePromise(vscode.window.onDidChangeActiveNotebookEditor); + const activeEditorClose = asPromise(vscode.window.onDidChangeActiveNotebookEditor); await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); await activeEditorClose; assert.strictEqual(secondEditor, vscode.window.activeNotebookEditor); @@ -399,12 +459,12 @@ suite('Notebook API tests', () => { test('notebook active editor change', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); - const firstEditorOpen = getEventOncePromise(vscode.window.onDidChangeActiveNotebookEditor); + const resource = await createRandomFile('', undefined, '.vsctestnb'); + const firstEditorOpen = asPromise(vscode.window.onDidChangeActiveNotebookEditor); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await firstEditorOpen; - const firstEditorDeactivate = getEventOncePromise(vscode.window.onDidChangeActiveNotebookEditor); + const firstEditorDeactivate = asPromise(vscode.window.onDidChangeActiveNotebookEditor); await vscode.commands.executeCommand('workbench.action.splitEditor'); await firstEditorDeactivate; @@ -413,10 +473,10 @@ suite('Notebook API tests', () => { test('edit API (replaceCells)', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - const cellsChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); + const cellsChangeEvent = asPromise(vscode.notebook.onDidChangeNotebookCells); await vscode.window.activeNotebookEditor!.edit(editBuilder => { editBuilder.replaceCells(1, 0, [{ cellKind: vscode.NotebookCellKind.Code, language: 'javascript', source: 'test 2', outputs: [], metadata: undefined }]); }); @@ -434,7 +494,7 @@ suite('Notebook API tests', () => { test('edit API (replaceOutput, USE NotebookCellOutput-type)', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.window.activeNotebookEditor!.edit(editBuilder => { @@ -463,7 +523,7 @@ suite('Notebook API tests', () => { test('edit API (replaceOutput)', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.window.activeNotebookEditor!.edit(editBuilder => { @@ -485,10 +545,10 @@ suite('Notebook API tests', () => { test('edit API (replaceOutput, event)', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - const outputChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeCellOutputs); + const outputChangeEvent = asPromise(vscode.notebook.onDidChangeCellOutputs); await vscode.window.activeNotebookEditor!.edit(editBuilder => { editBuilder.replaceCellOutput(0, [new vscode.NotebookCellOutput([ new vscode.NotebookCellOutputItem('foo', 'bar') @@ -511,7 +571,7 @@ suite('Notebook API tests', () => { test('edit API (replaceMetadata)', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.window.activeNotebookEditor!.edit(editBuilder => { @@ -530,10 +590,10 @@ suite('Notebook API tests', () => { test('edit API (replaceMetadata, event)', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - const event = getEventOncePromise(vscode.notebook.onDidChangeCellMetadata); + const event = asPromise(vscode.notebook.onDidChangeCellMetadata); await vscode.window.activeNotebookEditor!.edit(editBuilder => { editBuilder.replaceCellMetadata(0, { inputCollapsed: true, executionOrder: 17 }); @@ -548,141 +608,13 @@ suite('Notebook API tests', () => { await saveFileAndCloseAll(resource); }); - test('workspace edit API (replaceCells)', async function () { - - assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); - await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - - const { document } = vscode.window.activeNotebookEditor!; - assert.strictEqual(document.cells.length, 1); - - // inserting two new cells - { - const edit = new vscode.WorkspaceEdit(); - edit.replaceNotebookCells(document.uri, 0, 0, [{ - cellKind: vscode.NotebookCellKind.Markdown, - language: 'markdown', - metadata: undefined, - outputs: [], - source: 'new_markdown' - }, { - cellKind: vscode.NotebookCellKind.Code, - language: 'fooLang', - metadata: undefined, - outputs: [], - source: 'new_code' - }]); - - const success = await vscode.workspace.applyEdit(edit); - assert.strictEqual(success, true); - } - - assert.strictEqual(document.cells.length, 3); - assert.strictEqual(document.cells[0].document.getText(), 'new_markdown'); - assert.strictEqual(document.cells[1].document.getText(), 'new_code'); - - // deleting cell 1 and 3 - { - const edit = new vscode.WorkspaceEdit(); - edit.replaceNotebookCells(document.uri, 0, 1, []); - edit.replaceNotebookCells(document.uri, 2, 3, []); - const success = await vscode.workspace.applyEdit(edit); - assert.strictEqual(success, true); - } - - assert.strictEqual(document.cells.length, 1); - assert.strictEqual(document.cells[0].document.getText(), 'new_code'); - - // replacing all cells - { - const edit = new vscode.WorkspaceEdit(); - edit.replaceNotebookCells(document.uri, 0, 1, [{ - cellKind: vscode.NotebookCellKind.Markdown, - language: 'markdown', - metadata: undefined, - outputs: [], - source: 'new2_markdown' - }, { - cellKind: vscode.NotebookCellKind.Code, - language: 'fooLang', - metadata: undefined, - outputs: [], - source: 'new2_code' - }]); - const success = await vscode.workspace.applyEdit(edit); - assert.strictEqual(success, true); - } - assert.strictEqual(document.cells.length, 2); - assert.strictEqual(document.cells[0].document.getText(), 'new2_markdown'); - assert.strictEqual(document.cells[1].document.getText(), 'new2_code'); - - // remove all cells - { - const edit = new vscode.WorkspaceEdit(); - edit.replaceNotebookCells(document.uri, 0, document.cells.length, []); - const success = await vscode.workspace.applyEdit(edit); - assert.strictEqual(success, true); - } - assert.strictEqual(document.cells.length, 0); - - await saveFileAndCloseAll(resource); - }); - - test('workspace edit API (replaceCells, event)', async function () { - - assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); - await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - - const { document } = vscode.window.activeNotebookEditor!; - assert.strictEqual(document.cells.length, 1); - - const edit = new vscode.WorkspaceEdit(); - edit.replaceNotebookCells(document.uri, 0, 0, [{ - cellKind: vscode.NotebookCellKind.Markdown, - language: 'markdown', - metadata: undefined, - outputs: [], - source: 'new_markdown' - }, { - cellKind: vscode.NotebookCellKind.Code, - language: 'fooLang', - metadata: undefined, - outputs: [], - source: 'new_code' - }]); - - const event = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); - - const success = await vscode.workspace.applyEdit(edit); - assert.strictEqual(success, true); - - const data = await event; - - // check document - assert.strictEqual(document.cells.length, 3); - assert.strictEqual(document.cells[0].document.getText(), 'new_markdown'); - assert.strictEqual(document.cells[1].document.getText(), 'new_code'); - - // check event data - assert.strictEqual(data.document === document, true); - assert.strictEqual(data.changes.length, 1); - assert.strictEqual(data.changes[0].deletedCount, 0); - assert.strictEqual(data.changes[0].deletedItems.length, 0); - assert.strictEqual(data.changes[0].items.length, 2); - assert.strictEqual(data.changes[0].items[0], document.cells[0]); - assert.strictEqual(data.changes[0].items[1], document.cells[1]); - await saveFileAndCloseAll(resource); - }); - test('edit API batch edits', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - const cellsChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); - const cellMetadataChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeCellMetadata); + const cellsChangeEvent = asPromise(vscode.notebook.onDidChangeNotebookCells); + const cellMetadataChangeEvent = asPromise(vscode.notebook.onDidChangeCellMetadata); const version = vscode.window.activeNotebookEditor!.document.version; await vscode.window.activeNotebookEditor!.edit(editBuilder => { editBuilder.replaceCells(1, 0, [{ cellKind: vscode.NotebookCellKind.Code, language: 'javascript', source: 'test 2', outputs: [], metadata: undefined }]); @@ -697,11 +629,11 @@ suite('Notebook API tests', () => { test('edit API batch edits undo/redo', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - const cellsChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); - const cellMetadataChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeCellMetadata); + const cellsChangeEvent = asPromise(vscode.notebook.onDidChangeNotebookCells); + const cellMetadataChangeEvent = asPromise(vscode.notebook.onDidChangeCellMetadata); const version = vscode.window.activeNotebookEditor!.document.version; await vscode.window.activeNotebookEditor!.edit(editBuilder => { editBuilder.replaceCells(1, 0, [{ cellKind: vscode.NotebookCellKind.Code, language: 'javascript', source: 'test 2', outputs: [], metadata: undefined }]); @@ -724,7 +656,7 @@ suite('Notebook API tests', () => { test('initialzation should not emit cell change events.', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); let count = 0; const disposables: vscode.Disposable[] = []; @@ -739,12 +671,13 @@ suite('Notebook API tests', () => { await saveFileAndCloseAll(resource); }); -}); + // }); + + // suite('notebook workflow', () => { -suite('notebook workflow', () => { test('notebook open', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.strictEqual(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); assert.strictEqual(vscode.window.activeNotebookEditor!.selection?.document.getText(), 'test'); @@ -766,7 +699,7 @@ suite('notebook workflow', () => { test('notebook cell actions', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.strictEqual(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); assert.strictEqual(vscode.window.activeNotebookEditor!.selection?.document.getText(), 'test'); @@ -840,7 +773,7 @@ suite('notebook workflow', () => { test('notebook join cells', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.strictEqual(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); assert.strictEqual(vscode.window.activeNotebookEditor!.selection?.document.getText(), 'test'); @@ -852,7 +785,7 @@ suite('notebook workflow', () => { edit.insert(vscode.window.activeNotebookEditor!.selection!.uri, new vscode.Position(0, 0), 'var abc = 0;'); await vscode.workspace.applyEdit(edit); - const cellsChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); + const cellsChangeEvent = asPromise(vscode.notebook.onDidChangeNotebookCells); await vscode.commands.executeCommand('notebook.cell.joinAbove'); await cellsChangeEvent; @@ -864,7 +797,7 @@ suite('notebook workflow', () => { test('move cells will not recreate cells in ExtHost', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); @@ -884,7 +817,7 @@ suite('notebook workflow', () => { }); // test.only('document metadata is respected', async function () { - // const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + // const resource = await createRandomFile('', undefined, '.vsctestnb'); // await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); // assert.strictEqual(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); @@ -909,7 +842,7 @@ suite('notebook workflow', () => { test('cell runnable metadata is respected', async () => { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.strictEqual(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); const editor = vscode.window.activeNotebookEditor!; @@ -918,14 +851,14 @@ suite('notebook workflow', () => { const cell = editor.document.cells[0]; assert.strictEqual(cell.outputs.length, 0); - let metadataChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeCellMetadata); + let metadataChangeEvent = asPromise(vscode.notebook.onDidChangeCellMetadata); await updateCellMetadata(resource, cell, { ...cell.metadata, runnable: false }); await metadataChangeEvent; await vscode.commands.executeCommand('notebook.cell.execute'); assert.strictEqual(cell.outputs.length, 0, 'should not execute'); // not runnable, didn't work - metadataChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeCellMetadata); + metadataChangeEvent = asPromise(vscode.notebook.onDidChangeCellMetadata); await updateCellMetadata(resource, cell, { ...cell.metadata, runnable: true }); await metadataChangeEvent; @@ -938,7 +871,7 @@ suite('notebook workflow', () => { test('document runnable metadata is respected', async () => { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.strictEqual(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); const editor = vscode.window.activeNotebookEditor!; @@ -973,7 +906,7 @@ suite('notebook workflow', () => { // TODO@rebornix this is wrong, `await vscode.commands.executeCommand('notebook.execute');` doesn't wait until the workspace edit is applied test.skip('cell execute command takes arguments', async () => { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.strictEqual(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); const editor = vscode.window.activeNotebookEditor!; @@ -988,7 +921,7 @@ suite('notebook workflow', () => { test('cell execute command takes arguments 2', async () => { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.strictEqual(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); const editor = vscode.window.activeNotebookEditor!; @@ -1011,7 +944,7 @@ suite('notebook workflow', () => { assert.strictEqual(cell.outputs.length, 0, 'should clear'); }); - const secondResource = await createRandomFile('', undefined, 'second', '.vsctestnb'); + const secondResource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', secondResource, 'notebookCoreTest'); await withEvent(vscode.notebook.onDidChangeCellOutputs, async (event) => { @@ -1029,13 +962,13 @@ suite('notebook workflow', () => { test('document execute command takes arguments', async () => { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.strictEqual(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); const editor = vscode.window.activeNotebookEditor!; const cell = editor.document.cells[0]; - const metadataChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookDocumentMetadata); + const metadataChangeEvent = asPromise(vscode.notebook.onDidChangeNotebookDocumentMetadata); updateNotebookMetadata(editor.document.uri, { ...editor.document.metadata, runnable: true }); await metadataChangeEvent; assert.strictEqual(editor.document.metadata.runnable, true); @@ -1046,12 +979,12 @@ suite('notebook workflow', () => { assert.strictEqual(cell.outputs.length, 1, 'should execute'); // runnable, it worked }); - const clearChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeCellOutputs); + const clearChangeEvent = asPromise(vscode.notebook.onDidChangeCellOutputs); await vscode.commands.executeCommand('notebook.cell.clearOutputs'); await clearChangeEvent; assert.strictEqual(cell.outputs.length, 0, 'should clear'); - const secondResource = await createRandomFile('', undefined, 'second', '.vsctestnb'); + const secondResource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', secondResource, 'notebookCoreTest'); await withEvent(vscode.notebook.onDidChangeCellOutputs, async (event) => { @@ -1069,13 +1002,13 @@ suite('notebook workflow', () => { test('cell execute and select kernel', async () => { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.strictEqual(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); const editor = vscode.window.activeNotebookEditor!; const cell = editor.document.cells[0]; - const metadataChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookDocumentMetadata); + const metadataChangeEvent = asPromise(vscode.notebook.onDidChangeNotebookDocumentMetadata); updateNotebookMetadata(editor.document.uri, { ...editor.document.metadata, runnable: true }); await metadataChangeEvent; @@ -1087,7 +1020,7 @@ suite('notebook workflow', () => { 'my output' ]); - await vscode.commands.executeCommand('notebook.selectKernel', { extension: 'vscode.vscode-notebook-tests', id: 'secondaryKernel' }); + await vscode.commands.executeCommand('notebook.selectKernel', { extension: 'vscode.vscode-api-tests', id: 'secondaryKernel' }); await vscode.commands.executeCommand('notebook.cell.execute'); assert.strictEqual(cell.outputs.length, 1, 'should execute'); // runnable, it worked assert.strictEqual(cell.outputs[0].outputs.length, 1); @@ -1099,12 +1032,12 @@ suite('notebook workflow', () => { await vscode.commands.executeCommand('workbench.action.files.save'); await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); }); -}); + // }); -suite('notebook dirty state', () => { + // suite('notebook dirty state', () => { test('notebook open', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.strictEqual(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); assert.strictEqual(vscode.window.activeNotebookEditor!.selection?.document.getText(), 'test'); @@ -1133,12 +1066,12 @@ suite('notebook dirty state', () => { await saveFileAndCloseAll(resource); }); -}); + // }); -suite('notebook undo redo', () => { + // suite('notebook undo redo', () => { test('notebook open', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.strictEqual(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); assert.strictEqual(vscode.window.activeNotebookEditor!.selection?.document.getText(), 'test'); @@ -1181,7 +1114,7 @@ suite('notebook undo redo', () => { // test.skip('execute and then undo redo', async function () { // assertInitalState(); - // const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + // const resource = await createRandomFile('', undefined, '.vsctestnb'); // await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); // const cellsChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); @@ -1242,11 +1175,11 @@ suite('notebook undo redo', () => { // await saveFileAndCloseAll(resource); // }); -}); + // }); -suite('notebook working copy', () => { + // suite('notebook working copy', () => { // test('notebook revert on close', async function () { - // const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + // const resource = await createRandomFile('', undefined, '.vsctestnb'); // await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); // await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); // assert.strictEqual(vscode.window.activeNotebookEditor!.selection?.document.getText(), ''); @@ -1267,7 +1200,7 @@ suite('notebook working copy', () => { // }); // test('notebook revert', async function () { - // const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + // const resource = await createRandomFile('', undefined, '.vsctestnb'); // await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); // await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); // assert.strictEqual(vscode.window.activeNotebookEditor!.selection?.document.getText(), ''); @@ -1288,7 +1221,7 @@ suite('notebook working copy', () => { test('multiple tabs: dirty + clean', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); assert.strictEqual(vscode.window.activeNotebookEditor!.selection?.document.getText(), ''); @@ -1298,7 +1231,7 @@ suite('notebook working copy', () => { edit.insert(vscode.window.activeNotebookEditor!.selection!.uri, new vscode.Position(0, 0), 'var abc = 0;'); await vscode.workspace.applyEdit(edit); - const secondResource = await createRandomFile('', undefined, 'second', '.vsctestnb'); + const secondResource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', secondResource, 'notebookCoreTest'); await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); @@ -1314,7 +1247,7 @@ suite('notebook working copy', () => { test('multiple tabs: two dirty tabs and switching', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); assert.strictEqual(vscode.window.activeNotebookEditor!.selection?.document.getText(), ''); @@ -1324,7 +1257,7 @@ suite('notebook working copy', () => { edit.insert(vscode.window.activeNotebookEditor!.selection!.uri, new vscode.Position(0, 0), 'var abc = 0;'); await vscode.workspace.applyEdit(edit); - const secondResource = await createRandomFile('', undefined, 'second', '.vsctestnb'); + const secondResource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', secondResource, 'notebookCoreTest'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); assert.strictEqual(vscode.window.activeNotebookEditor!.selection?.document.getText(), ''); @@ -1353,7 +1286,7 @@ suite('notebook working copy', () => { test('multiple tabs: different editors with same document', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); const firstNotebookEditor = vscode.window.activeNotebookEditor; assert.strictEqual(firstNotebookEditor !== undefined, true, 'notebook first'); @@ -1375,12 +1308,12 @@ suite('notebook working copy', () => { // await vscode.commands.executeCommand('workbench.action.files.saveAll'); // await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); -}); + // }); -suite('metadata', () => { + // suite('metadata', () => { test('custom metadata should be supported', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.strictEqual(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); assert.strictEqual(vscode.window.activeNotebookEditor!.document.metadata.custom!['testMetadata'] as boolean, false); @@ -1394,7 +1327,7 @@ suite('metadata', () => { // TODO@rebornix skip as it crashes the process all the time test.skip('custom metadata should be supported 2', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.strictEqual(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); assert.strictEqual(vscode.window.activeNotebookEditor!.document.metadata.custom!['testMetadata'] as boolean, false); @@ -1409,9 +1342,9 @@ suite('metadata', () => { await saveFileAndCloseAll(resource); }); -}); + // }); -suite('regression', () => { + // suite('regression', () => { // test('microsoft/vscode-github-issue-notebooks#26. Insert template cell in the new empty document', async function () { // assertInitalState(); // await vscode.commands.executeCommand('workbench.action.files.newUntitledFile', { "viewType": "notebookCoreTest" }); @@ -1423,7 +1356,7 @@ suite('regression', () => { test('#106657. Opening a notebook from markers view is broken ', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); const document = vscode.window.activeNotebookEditor?.document!; @@ -1441,7 +1374,7 @@ suite('regression', () => { test.skip('Cannot open notebook from cell-uri with vscode.open-command', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); const document = vscode.window.activeNotebookEditor?.document!; @@ -1459,7 +1392,7 @@ suite('regression', () => { test('#97830, #97764. Support switch to other editor types', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'empty', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); const edit = new vscode.WorkspaceEdit(); @@ -1469,12 +1402,9 @@ suite('regression', () => { assert.strictEqual(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); assert.strictEqual(vscode.window.activeNotebookEditor!.selection?.document.getText(), 'var abc = 0;'); - // todo@jrieken enforce a kernel (how) and test that its language is picked - // assert.strictEqual(vscode.window.activeNotebookEditor!.selection?.language, 'typescript'); - // no kernel -> no default language assert.strictEqual(vscode.window.activeNotebookEditor!.kernel, undefined); - assert.strictEqual(vscode.window.activeNotebookEditor!.selection?.language, 'plaintext'); + assert.strictEqual(vscode.window.activeNotebookEditor!.selection?.language, 'typescript'); await vscode.commands.executeCommand('vscode.openWith', resource, 'default'); assert.strictEqual(vscode.window.activeTextEditor?.document.uri.path, resource.path); @@ -1485,7 +1415,7 @@ suite('regression', () => { // open text editor, pin, and then open a notebook test('#96105 - dirty editors', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'empty', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'default'); const edit = new vscode.WorkspaceEdit(); edit.insert(resource, new vscode.Position(0, 0), 'var abc = 0;'); @@ -1509,7 +1439,7 @@ suite('regression', () => { test('#102423 - copy/paste shares the same text buffer', async function () { assertInitalState(); - const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); let activeCell = vscode.window.activeNotebookEditor!.selection; @@ -1530,16 +1460,16 @@ suite('regression', () => { await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); -}); + // }); -suite('webview', () => { + // suite('webview', () => { // for web, `asWebUri` gets `https`? // test('asWebviewUri', async function () { // if (vscode.env.uiKind === vscode.UIKind.Web) { // return; // } - // const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + // const resource = await createRandomFile('', undefined, '.vsctestnb'); // await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); // assert.strictEqual(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); // const uri = vscode.window.activeNotebookEditor!.asWebviewUri(vscode.Uri.file('./hello.png')); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts index 65fd89d3b96..615bb79ac6b 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts @@ -150,6 +150,13 @@ suite('vscode API - window', () => { }); test('active editor not always correct... #49125', async function () { + + if (!window.state.focused) { + // no focus! + this.skip(); + return; + } + if (process.env['BUILD_SOURCEVERSION']) { this.skip(); return; diff --git a/extensions/vscode-api-tests/src/utils.ts b/extensions/vscode-api-tests/src/utils.ts index f7923bbc9f7..8d94c8ea120 100644 --- a/extensions/vscode-api-tests/src/utils.ts +++ b/extensions/vscode-api-tests/src/utils.ts @@ -17,7 +17,7 @@ vscode.workspace.registerFileSystemProvider(testFs.scheme, testFs, { isCaseSensi export async function createRandomFile(contents = '', dir: vscode.Uri | undefined = undefined, ext = ''): Promise { let fakeFile: vscode.Uri; if (dir) { - assert.equal(dir.scheme, testFs.scheme); + assert.strictEqual(dir.scheme, testFs.scheme); fakeFile = dir.with({ path: dir.path + '/' + rndName() + ext }); } else { fakeFile = vscode.Uri.parse(`${testFs.scheme}:/${rndName() + ext}`); @@ -48,6 +48,10 @@ export function closeAllEditors(): Thenable { return vscode.commands.executeCommand('workbench.action.closeAllEditors'); } +export function saveAllEditors(): Thenable { + return vscode.commands.executeCommand('workbench.action.files.saveAll'); +} + export async function revertAllDirty(): Promise { return vscode.commands.executeCommand('_workbench.revertAllDirty'); } @@ -117,3 +121,19 @@ export function assertNoRpcFromEntry(entry: [obj: any, name: string]) { assert.strictEqual(rpcPaths.length, 0, rpcPaths.join('\n')); assert.strictEqual(proxyPaths.length, 0, proxyPaths.join('\n')); // happens... } + +export async function asPromise(event: vscode.Event, timeout = 5000): Promise { + return new Promise((resolve, reject) => { + + const handle = setTimeout(() => { + sub.dispose(); + reject(new Error('asPromise TIMEOUT reached')); + }, timeout); + + const sub = event(e => { + clearTimeout(handle); + sub.dispose(); + resolve(e); + }); + }); +} diff --git a/extensions/vscode-notebook-tests/package.json b/extensions/vscode-notebook-tests/package.json index 0451ebf9283..153ce130e24 100644 --- a/extensions/vscode-notebook-tests/package.json +++ b/extensions/vscode-notebook-tests/package.json @@ -34,16 +34,6 @@ } ], "notebookProvider": [ - { - "viewType": "notebookCoreTest", - "displayName": "Notebook Core Test", - "selector": [ - { - "filenamePattern": "*.vsctestnb", - "excludeFileNamePattern": "" - } - ] - }, { "viewType": "notebookSmokeTest", "displayName": "Notebook Smoke Test", diff --git a/extensions/vscode-notebook-tests/src/notebookTestMain.ts b/extensions/vscode-notebook-tests/src/notebookTestMain.ts index c856d19e46c..8cf19b430be 100644 --- a/extensions/vscode-notebook-tests/src/notebookTestMain.ts +++ b/extensions/vscode-notebook-tests/src/notebookTestMain.ts @@ -9,128 +9,6 @@ import { smokeTestActivate } from './notebookSmokeTestMain'; export function activate(context: vscode.ExtensionContext): any { smokeTestActivate(context); - context.subscriptions.push(vscode.notebook.registerNotebookContentProvider('notebookCoreTest', { - openNotebook: async (_resource: vscode.Uri): Promise => { - if (/.*empty\-.*\.vsctestnb$/.test(_resource.path)) { - return { - metadata: {}, - cells: [] - }; - } - const dto: vscode.NotebookData = { - metadata: { - custom: { testMetadata: false } - }, - cells: [ - { - source: 'test', - language: 'typescript', - cellKind: vscode.NotebookCellKind.Code, - outputs: [], - metadata: { - custom: { testCellMetadata: 123 } - } - } - ] - }; - return dto; - }, - resolveNotebook: async (_document: vscode.NotebookDocument) => { - return; - }, - saveNotebook: async (_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { - return; - }, - saveNotebookAs: async (_targetResource: vscode.Uri, _document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { - return; - }, - backupNotebook: async (_document: vscode.NotebookDocument, _context: vscode.NotebookDocumentBackupContext, _cancellation: vscode.CancellationToken) => { - return { - id: '1', - delete: () => { } - }; - } - })); - const kernel: vscode.NotebookKernel = { - id: 'mainKernel', - label: 'Notebook Test Kernel', - isPreferred: true, - supportedLanguages: ['typescript'], - executeAllCells: async (_document: vscode.NotebookDocument) => { - const edit = new vscode.WorkspaceEdit(); - - edit.replaceNotebookCellOutput(_document.uri, 0, [new vscode.NotebookCellOutput([ - new vscode.NotebookCellOutputItem('text/plain', ['my output'], undefined) - ])]); - return vscode.workspace.applyEdit(edit); - }, - cancelAllCellsExecution: async (_document: vscode.NotebookDocument) => { }, - executeCell: async (document: vscode.NotebookDocument, cell: vscode.NotebookCell | undefined) => { - if (!cell) { - cell = document.cells[0]; - } - - if (document.uri.path.endsWith('customRenderer.vsctestnb')) { - const edit = new vscode.WorkspaceEdit(); - edit.replaceNotebookCellOutput(document.uri, cell.index, [new vscode.NotebookCellOutput([ - new vscode.NotebookCellOutputItem('text/custom', ['test'], undefined) - ])]); - - return vscode.workspace.applyEdit(edit); - } - - const edit = new vscode.WorkspaceEdit(); - // const previousOutputs = cell.outputs; - edit.replaceNotebookCellOutput(document.uri, cell.index, [new vscode.NotebookCellOutput([ - new vscode.NotebookCellOutputItem('text/plain', ['my output'], undefined) - ])]); - - return vscode.workspace.applyEdit(edit); - }, - cancelCellExecution: async (_document: vscode.NotebookDocument, _cell: vscode.NotebookCell) => { } - }; - - const kernel2: vscode.NotebookKernel = { - id: 'secondaryKernel', - label: 'Notebook Secondary Test Kernel', - isPreferred: false, - supportedLanguages: ['typescript'], - executeAllCells: async (_document: vscode.NotebookDocument) => { - const edit = new vscode.WorkspaceEdit(); - edit.replaceNotebookCellOutput(_document.uri, 0, [new vscode.NotebookCellOutput([ - new vscode.NotebookCellOutputItem('text/plain', ['my second output'], undefined) - ])]); - - return vscode.workspace.applyEdit(edit); - }, - cancelAllCellsExecution: async (_document: vscode.NotebookDocument) => { }, - executeCell: async (document: vscode.NotebookDocument, cell: vscode.NotebookCell | undefined) => { - if (!cell) { - cell = document.cells[0]; - } - - const edit = new vscode.WorkspaceEdit(); - - if (document.uri.path.endsWith('customRenderer.vsctestnb')) { - edit.replaceNotebookCellOutput(document.uri, cell.index, [new vscode.NotebookCellOutput([ - new vscode.NotebookCellOutputItem('text/custom', ['test 2'], undefined) - ])]); - } else { - edit.replaceNotebookCellOutput(document.uri, cell.index, [new vscode.NotebookCellOutput([ - new vscode.NotebookCellOutputItem('text/plain', ['my second output'], undefined) - ])]); - } - - return vscode.workspace.applyEdit(edit); - }, - cancelCellExecution: async (_document: vscode.NotebookDocument, _cell: vscode.NotebookCell) => { } - }; - - context.subscriptions.push(vscode.notebook.registerNotebookKernelProvider({ filenamePattern: '*.vsctestnb' }, { - provideKernels: async () => { - return [kernel, kernel2]; - } - })); } diff --git a/product.json b/product.json index 47bb865de2e..e9561e59ee2 100644 --- a/product.json +++ b/product.json @@ -5,7 +5,7 @@ "dataFolderName": ".vscode-oss", "win32MutexName": "vscodeoss", "licenseName": "MIT", - "licenseUrl": "https://github.com/microsoft/vscode/blob/master/LICENSE.txt", + "licenseUrl": "https://github.com/microsoft/vscode/blob/main/LICENSE.txt", "win32DirName": "Microsoft Code OSS", "win32NameVersion": "Microsoft Code OSS", "win32RegValueName": "CodeOSS", diff --git a/scripts/generate-definitelytyped.sh b/scripts/generate-definitelytyped.sh index 118401b43cb..1b139ebbf79 100755 --- a/scripts/generate-definitelytyped.sh +++ b/scripts/generate-definitelytyped.sh @@ -14,7 +14,7 @@ header="// Type definitions for Visual Studio Code ${1} /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. - * See https://github.com/microsoft/vscode/blob/master/LICENSE.txt for license information. + * See https://github.com/microsoft/vscode/blob/main/LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ /** diff --git a/src/main.js b/src/main.js index 0541fb4c4a0..2584223e1d6 100644 --- a/src/main.js +++ b/src/main.js @@ -9,14 +9,14 @@ const perf = require('./vs/base/common/performance'); perf.mark('code/didStartMain'); -const lp = require('./vs/base/node/languagePacks'); const path = require('path'); const fs = require('fs'); const os = require('os'); +const { getNLSConfiguration } = require('./vs/base/node/languagePacks'); const bootstrap = require('./bootstrap'); const bootstrapNode = require('./bootstrap-node'); -const paths = require('./paths'); -/** @type {Partial & { applicationName: string}} */ +const { getDefaultUserDataPath } = require('./vs/base/node/userDataPath'); +/** @type {Partial} */ const product = require('../product.json'); const { app, protocol, crashReporter } = require('electron'); @@ -84,7 +84,7 @@ let nlsConfigurationPromise = undefined; const metaDataFile = path.join(__dirname, 'nls.metadata.json'); const locale = getUserDefinedLocale(argvConfig); if (locale) { - nlsConfigurationPromise = lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, locale); + nlsConfigurationPromise = getNLSConfiguration(product.commit, userDataPath, metaDataFile, locale); } // Load our code once ready @@ -405,7 +405,7 @@ function getUserDataPath(cliArgs) { return path.join(portable.portableDataPath, 'user-data'); } - return path.resolve(cliArgs['user-data-dir'] || paths.getDefaultUserDataPath()); + return path.resolve(cliArgs['user-data-dir'] || getDefaultUserDataPath()); } /** @@ -560,7 +560,7 @@ async function resolveNlsConfiguration() { // See above the comment about the loader and case sensitiviness appLocale = appLocale.toLowerCase(); - nlsConfiguration = await lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, appLocale); + nlsConfiguration = await getNLSConfiguration(product.commit, userDataPath, metaDataFile, appLocale); if (!nlsConfiguration) { nlsConfiguration = { locale: appLocale, availableLanguages: {} }; } diff --git a/src/paths.js b/src/paths.js deleted file mode 100644 index 2042123d726..00000000000 --- a/src/paths.js +++ /dev/null @@ -1,48 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -//@ts-check -'use strict'; - -const pkg = require('../package.json'); -const path = require('path'); -const os = require('os'); - -/** - * @returns {string} - */ -function getDefaultUserDataPath() { - - // Support global VSCODE_APPDATA environment variable - let appDataPath = process.env['VSCODE_APPDATA']; - - // Otherwise check per platform - if (!appDataPath) { - switch (process.platform) { - case 'win32': - appDataPath = process.env['APPDATA']; - if (!appDataPath) { - const userProfile = process.env['USERPROFILE']; - if (typeof userProfile !== 'string') { - throw new Error('Windows: Unexpected undefined %USERPROFILE% environment variable'); - } - appDataPath = path.join(userProfile, 'AppData', 'Roaming'); - } - break; - case 'darwin': - appDataPath = path.join(os.homedir(), 'Library', 'Application Support'); - break; - case 'linux': - appDataPath = process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config'); - break; - default: - throw new Error('Platform not supported'); - } - } - - return path.join(appDataPath, pkg.name); -} - -exports.getDefaultUserDataPath = getDefaultUserDataPath; diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 81457733d22..5378627e57a 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -383,5 +383,16 @@ export function renderMarkdownAsPlaintext(markdown: IMarkdownString) { if (value.length > 100_000) { value = `${value.substr(0, 100_000)}…`; } - return sanitizeRenderedMarkdown({ isTrusted: false }, marked.parse(value, { renderer })).toString(); + + const unescapeInfo = new Map([ + ['"', '"'], + ['&', '&'], + [''', '\''], + ['<', '<'], + ['>', '>'], + ]); + + const html = marked.parse(value, { renderer }).replace(/&(#\d+|[a-zA-Z]+);/g, m => unescapeInfo.get(m) ?? m); + + return sanitizeRenderedMarkdown({ isTrusted: false }, html).toString(); } diff --git a/src/vs/base/browser/ui/actionbar/actionViewItems.ts b/src/vs/base/browser/ui/actionbar/actionViewItems.ts index b4dafee88bf..9abeda1c547 100644 --- a/src/vs/base/browser/ui/actionbar/actionViewItems.ts +++ b/src/vs/base/browser/ui/actionbar/actionViewItems.ts @@ -14,7 +14,7 @@ import { EventType as TouchEventType, 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'; -import { $, addDisposableListener, append, EventHelper, EventLike, EventType, removeTabIndexAndUpdateFocus } from 'vs/base/browser/dom'; +import { $, addDisposableListener, append, EventHelper, EventLike, EventType } from 'vs/base/browser/dom'; export interface IBaseActionViewItemOptions { draggable?: boolean; @@ -181,9 +181,9 @@ export class BaseActionViewItem extends Disposable implements IActionViewItem { } } - setFocusable(): void { + setFocusable(focusable: boolean): void { if (this.element) { - this.element.tabIndex = 0; + this.element.tabIndex = focusable ? 0 : -1; } } @@ -288,9 +288,9 @@ export class ActionViewItem extends BaseActionViewItem { } } - setFocusable(): void { + setFocusable(focusable: boolean): void { if (this.label) { - this.label.tabIndex = 0; + this.label.tabIndex = focusable ? 0 : -1; } } @@ -356,7 +356,6 @@ export class ActionViewItem extends BaseActionViewItem { if (this.label) { this.label.setAttribute('aria-disabled', 'true'); this.label.classList.add('disabled'); - removeTabIndexAndUpdateFocus(this.label); } if (this.element) { diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index e02edc21588..83135785546 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -35,7 +35,8 @@ export interface IActionBarOptions { readonly triggerKeys?: ActionTrigger; readonly allowContextMenu?: boolean; readonly preventLoopNavigation?: boolean; - readonly ignoreOrientationForPreviousAndNextKey?: boolean; + // Pass true here when the up and down keys should not be eaten up by the ActionBar. For example, when an ActionBar is in the list. + readonly respectOrientationForPreviousAndNextKey?: boolean; } export interface IActionOptions extends IActionViewItemOptions { @@ -63,6 +64,8 @@ export class ActionBar extends Disposable implements IActionRunner { // Trigger Key Tracking private triggerKeyDown: boolean = false; + private focusable: boolean = true; + // Elements domNode: HTMLElement; protected actionsList: HTMLElement; @@ -117,22 +120,22 @@ export class ActionBar extends Disposable implements IActionRunner { switch (this._orientation) { case ActionsOrientation.HORIZONTAL: - previousKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.LeftArrow, KeyCode.UpArrow] : [KeyCode.LeftArrow]; - nextKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.RightArrow, KeyCode.DownArrow] : [KeyCode.RightArrow]; + previousKeys = this.options.respectOrientationForPreviousAndNextKey ? [KeyCode.LeftArrow] : [KeyCode.LeftArrow, KeyCode.UpArrow]; + nextKeys = this.options.respectOrientationForPreviousAndNextKey ? [KeyCode.RightArrow] : [KeyCode.RightArrow, KeyCode.DownArrow]; break; case ActionsOrientation.HORIZONTAL_REVERSE: - previousKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.RightArrow, KeyCode.DownArrow] : [KeyCode.RightArrow]; - nextKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.LeftArrow, KeyCode.UpArrow] : [KeyCode.LeftArrow]; + previousKeys = this.options.respectOrientationForPreviousAndNextKey ? [KeyCode.RightArrow] : [KeyCode.RightArrow, KeyCode.DownArrow]; + nextKeys = this.options.respectOrientationForPreviousAndNextKey ? [KeyCode.LeftArrow] : [KeyCode.LeftArrow, KeyCode.UpArrow]; this.domNode.className += ' reverse'; break; case ActionsOrientation.VERTICAL: - previousKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.LeftArrow, KeyCode.UpArrow] : [KeyCode.UpArrow]; - nextKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.RightArrow, KeyCode.DownArrow] : [KeyCode.DownArrow]; + previousKeys = this.options.respectOrientationForPreviousAndNextKey ? [KeyCode.UpArrow] : [KeyCode.LeftArrow, KeyCode.UpArrow]; + nextKeys = this.options.respectOrientationForPreviousAndNextKey ? [KeyCode.DownArrow] : [KeyCode.RightArrow, KeyCode.DownArrow]; this.domNode.className += ' vertical'; break; case ActionsOrientation.VERTICAL_REVERSE: - previousKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.RightArrow, KeyCode.DownArrow] : [KeyCode.DownArrow]; - nextKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.LeftArrow, KeyCode.UpArrow] : [KeyCode.UpArrow]; + previousKeys = this.options.respectOrientationForPreviousAndNextKey ? [KeyCode.DownArrow] : [KeyCode.RightArrow, KeyCode.DownArrow]; + nextKeys = this.options.respectOrientationForPreviousAndNextKey ? [KeyCode.UpArrow] : [KeyCode.LeftArrow, KeyCode.UpArrow]; this.domNode.className += ' vertical reverse'; break; } @@ -219,6 +222,25 @@ export class ActionBar extends Disposable implements IActionRunner { } } + // Some action bars should not be focusable at times + // When an action bar is not focusable make sure to make all the elements inside it not focusable + // When an action bar is focusable again, make sure the first item can be focused + setFocusable(focusable: boolean): void { + this.focusable = focusable; + if (this.focusable) { + const first = this.viewItems.find(vi => vi instanceof BaseActionViewItem); + if (first instanceof BaseActionViewItem) { + first.setFocusable(true); + } + } else { + this.viewItems.forEach(vi => { + if (vi instanceof BaseActionViewItem) { + vi.setFocusable(false); + } + }); + } + } + private isTriggerKeyEvent(event: StandardKeyboardEvent): boolean { let ret = false; this._triggerKeys.keys.forEach(keyCode => { @@ -297,9 +319,9 @@ export class ActionBar extends Disposable implements IActionRunner { item.setActionContext(this.context); item.render(actionViewItemElement); - if (this.viewItems.every(i => !i.isEnabled()) && item instanceof BaseActionViewItem && item.isEnabled()) { + if (this.focusable && this.viewItems.length === 0 && item instanceof BaseActionViewItem) { // We need to allow for the first enabled item to be focused on using tab navigation #106441 - item.setFocusable(); + item.setFocusable(true); } if (index === null || index < 0 || index >= this.actionsList.children.length) { diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index ebfe8d59f17..473eb2a2ec4 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -12,7 +12,7 @@ import { Event as BaseEvent, Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Gesture, EventType as TouchEventType } from 'vs/base/browser/touch'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { addDisposableListener, IFocusTracker, EventType, EventHelper, trackFocus, reset, removeTabIndexAndUpdateFocus } from 'vs/base/browser/dom'; +import { addDisposableListener, IFocusTracker, EventType, EventHelper, trackFocus, reset } from 'vs/base/browser/dom'; import { IContextMenuProvider } from 'vs/base/browser/contextmenu'; import { Action, IAction, IActionRunner } from 'vs/base/common/actions'; import { CSSIcon, Codicon } from 'vs/base/common/codicons'; @@ -214,7 +214,6 @@ export class Button extends Disposable implements IButton { } else { this._element.classList.add('disabled'); this._element.setAttribute('aria-disabled', String(true)); - removeTabIndexAndUpdateFocus(this._element); } } diff --git a/src/vs/base/browser/ui/checkbox/checkbox.ts b/src/vs/base/browser/ui/checkbox/checkbox.ts index cd9e4a2718c..d9319424fc5 100644 --- a/src/vs/base/browser/ui/checkbox/checkbox.ts +++ b/src/vs/base/browser/ui/checkbox/checkbox.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./checkbox'; -import * as DOM from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Widget } from 'vs/base/browser/ui/widget'; import { Color } from 'vs/base/common/color'; @@ -76,6 +75,26 @@ export class CheckboxActionViewItem extends BaseActionViewItem { } } + focus(): void { + if (this.checkbox) { + this.checkbox.domNode.tabIndex = 0; + this.checkbox.focus(); + } + } + + blur(): void { + if (this.checkbox) { + this.checkbox.domNode.tabIndex = -1; + this.checkbox.domNode.blur(); + } + } + + setFocusable(focusable: boolean): void { + if (this.checkbox) { + this.checkbox.domNode.tabIndex = focusable ? 0 : -1; + } + } + dispose(): void { this.disposables.dispose(); super.dispose(); @@ -191,12 +210,10 @@ export class Checkbox extends Widget { } enable(): void { - this.domNode.tabIndex = 0; this.domNode.setAttribute('aria-disabled', String(false)); } disable(): void { - DOM.removeTabIndexAndUpdateFocus(this.domNode); this.domNode.setAttribute('aria-disabled', String(true)); } } diff --git a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts index 353b766ef1d..2aaac8a7ab3 100644 --- a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts +++ b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts @@ -179,5 +179,4 @@ export class ActionWithDropdownActionViewItem extends ActionViewItem { this.dropdownMenuActionViewItem.render(this.element); } } - } diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 81caae42707..d399ef27bd7 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -761,6 +761,11 @@ export class DefaultStyleController implements IStyleController { `); } + if (styles.listInactiveFocusForeground) { + content.push(`.monaco-list${suffix} .monaco-list-row.focused { color: ${styles.listInactiveFocusForeground}; }`); + content.push(`.monaco-list${suffix} .monaco-list-row.focused:hover { color: ${styles.listInactiveFocusForeground}; }`); // overwrite :hover style in this case! + } + if (styles.listInactiveFocusBackground) { content.push(`.monaco-list${suffix} .monaco-list-row.focused { background-color: ${styles.listInactiveFocusBackground}; }`); content.push(`.monaco-list${suffix} .monaco-list-row.focused:hover { background-color: ${styles.listInactiveFocusBackground}; }`); // overwrite :hover style in this case! @@ -776,7 +781,7 @@ export class DefaultStyleController implements IStyleController { } if (styles.listHoverBackground) { - content.push(`.monaco-list${suffix}:not(.drop-target) .monaco-list-row:hover:not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`); + content.push(`.monaco-list${suffix}:not(.drop-target) .monaco-list-row:hover:not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`); } if (styles.listHoverForeground) { @@ -867,6 +872,7 @@ export interface IListStyles { listFocusAndSelectionForeground?: Color; listInactiveSelectionBackground?: Color; listInactiveSelectionForeground?: Color; + listInactiveFocusForeground?: Color; listInactiveFocusBackground?: Color; listHoverBackground?: Color; listHoverForeground?: Color; diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index 947ba2708b0..9a9132359ce 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -8,7 +8,7 @@ import * as strings from 'vs/base/common/strings'; import { IActionRunner, IAction, SubmenuAction, Separator, IActionViewItemProvider, EmptySubmenuAction } from 'vs/base/common/actions'; import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes'; -import { EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor, addDisposableListener, append, $, clearNode, createStyleSheet, isInShadowDOM, getActiveElement, Dimension, IDomNodePagePosition } from 'vs/base/browser/dom'; +import { EventType, EventHelper, EventLike, isAncestor, addDisposableListener, append, $, 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'; @@ -86,6 +86,7 @@ export class Menu extends ActionBar { context: options.context, actionRunner: options.actionRunner, ariaLabel: options.ariaLabel, + respectOrientationForPreviousAndNextKey: true, triggerKeys: { keys: [KeyCode.Enter, ...(isMacintosh || isLinux ? [KeyCode.Space] : [])], keyDown: true } }); @@ -617,20 +618,23 @@ class BaseMenuActionViewItem extends BaseActionViewItem { if (this.getAction().enabled) { if (this.element) { this.element.classList.remove('disabled'); + this.element.removeAttribute('aria-disabled'); } if (this.item) { this.item.classList.remove('disabled'); + this.item.removeAttribute('aria-disabled'); this.item.tabIndex = 0; } } else { if (this.element) { this.element.classList.add('disabled'); + this.element.setAttribute('aria-disabled', 'true'); } if (this.item) { this.item.classList.add('disabled'); - removeTabIndexAndUpdateFocus(this.item); + this.item.setAttribute('aria-disabled', 'true'); } } } diff --git a/src/vs/base/browser/ui/menu/menubar.css b/src/vs/base/browser/ui/menu/menubar.css index bfa79a1c523..e6aa5f4ddbe 100644 --- a/src/vs/base/browser/ui/menu/menubar.css +++ b/src/vs/base/browser/ui/menu/menubar.css @@ -42,7 +42,7 @@ } .menubar .menubar-menu-items-holder { - position: absolute; + position: fixed; left: 0px; opacity: 1; z-index: 2000; diff --git a/src/vs/base/browser/ui/sash/sash.ts b/src/vs/base/browser/ui/sash/sash.ts index f82138b8626..bceaf6e4e9b 100644 --- a/src/vs/base/browser/ui/sash/sash.ts +++ b/src/vs/base/browser/ui/sash/sash.ts @@ -398,13 +398,21 @@ export class Sash extends Disposable { })); } - private static onMouseEnter(sash: Sash): void { + private static onMouseEnter(sash: Sash, fromLinkedSash: boolean = false): void { sash.hoverDelayer.trigger(() => sash.el.classList.add('hover')); + + if (!fromLinkedSash && sash.linkedSash) { + Sash.onMouseEnter(sash.linkedSash, true); + } } - private static onMouseLeave(sash: Sash): void { + private static onMouseLeave(sash: Sash, fromLinkedSash: boolean = false): void { sash.hoverDelayer.cancel(); sash.el.classList.remove('hover'); + + if (!fromLinkedSash && sash.linkedSash) { + Sash.onMouseLeave(sash.linkedSash, true); + } } layout(): void { diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index 841f2ee44b0..9a2fab22e13 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -736,8 +736,8 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.Enter).on(e => this.onEnter(e), this)); this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.Escape).on(e => this.onEscape(e), this)); - this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.UpArrow).on(this.onUpArrow, this)); - this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.DownArrow).on(this.onDownArrow, this)); + this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.UpArrow).on(e => this.onUpArrow(e), this)); + this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.DownArrow).on(e => this.onDownArrow(e), this)); this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.PageDown).on(this.onPageDown, this)); this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.PageUp).on(this.onPageUp, this)); this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.Home).on(this.onHome, this)); @@ -916,8 +916,9 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi } // List navigation - have to handle a disabled option (jump over) - private onDownArrow(): void { + private onDownArrow(e: StandardKeyboardEvent): void { if (this.selected < this.options.length - 1) { + dom.EventHelper.stop(e, true); // Skip disabled options const nextOptionDisabled = this.options[this.selected + 1].isDisabled; @@ -937,8 +938,9 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi } } - private onUpArrow(): void { + private onUpArrow(e: StandardKeyboardEvent): void { if (this.selected > 0) { + dom.EventHelper.stop(e, true); // Skip disabled options const previousOptionDisabled = this.options[this.selected - 1].isDisabled; if (previousOptionDisabled && this.selected > 1) { diff --git a/src/vs/base/browser/ui/toolbar/toolbar.ts b/src/vs/base/browser/ui/toolbar/toolbar.ts index c1348a93ddc..3b91ffa5573 100644 --- a/src/vs/base/browser/ui/toolbar/toolbar.ts +++ b/src/vs/base/browser/ui/toolbar/toolbar.ts @@ -28,6 +28,7 @@ export interface IToolBarOptions { anchorAlignmentProvider?: () => AnchorAlignment; renderDropdownAsChildElement?: boolean; moreIcon?: CSSIcon; + readonly respectOrientationForPreviousAndNextKey?: boolean; } /** @@ -63,6 +64,7 @@ export class ToolBar extends Disposable { orientation: options.orientation, ariaLabel: options.ariaLabel, actionRunner: options.actionRunner, + respectOrientationForPreviousAndNextKey: options.respectOrientationForPreviousAndNextKey, actionViewItemProvider: (action: IAction) => { if (action.id === ToggleMenuAction.ID) { this.toggleMenuActionViewItem = new DropdownMenuActionViewItem( diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 4df19745cfe..af081547c85 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -961,7 +961,7 @@ export interface IAbstractTreeOptionsUpdate extends ITreeRendererOptions { readonly filterOnType?: boolean; readonly smoothScrolling?: boolean; readonly horizontalScrolling?: boolean; - readonly expandOnlyOnDoubleClick?: boolean; + readonly expandOnDoubleClick?: boolean; readonly expandOnlyOnTwistieClick?: boolean | ((e: any) => boolean); // e is T } @@ -1121,7 +1121,7 @@ class TreeNodeListMouseController extends MouseController< return super.onViewPointer(e); } - if (this.tree.expandOnlyOnDoubleClick && e.browserEvent.detail !== 2 && !onTwistie) { + if (!this.tree.expandOnDoubleClick && e.browserEvent.detail === 2) { return super.onViewPointer(e); } @@ -1129,6 +1129,7 @@ class TreeNodeListMouseController extends MouseController< const model = ((this.tree as any).model as ITreeModel); // internal const location = model.getNodeLocation(node); const recursive = e.browserEvent.altKey; + this.tree.setFocus([location]); model.setCollapsed(location, undefined, recursive); if (expandOnlyOnTwistieClick && onTwistie) { @@ -1142,7 +1143,7 @@ class TreeNodeListMouseController extends MouseController< protected onDoubleClick(e: IListMouseEvent>): void { const onTwistie = (e.browserEvent.target as HTMLElement).classList.contains('monaco-tl-twistie'); - if (onTwistie) { + if (onTwistie || !this.tree.expandOnDoubleClick) { return; } @@ -1262,8 +1263,8 @@ export abstract class AbstractTree implements IDisposable get filterOnType(): boolean { return !!this._options.filterOnType; } get onDidChangeTypeFilterPattern(): Event { return this.typeFilterController ? this.typeFilterController.onDidChangePattern : Event.None; } - get expandOnlyOnDoubleClick(): boolean { return this._options.expandOnlyOnDoubleClick ?? false; } - get expandOnlyOnTwistieClick(): boolean | ((e: T) => boolean) { return typeof this._options.expandOnlyOnTwistieClick === 'undefined' ? false : this._options.expandOnlyOnTwistieClick; } + get expandOnDoubleClick(): boolean { return typeof this._options.expandOnDoubleClick === 'undefined' ? true : this._options.expandOnDoubleClick; } + get expandOnlyOnTwistieClick(): boolean | ((e: T) => boolean) { return typeof this._options.expandOnlyOnTwistieClick === 'undefined' ? true : this._options.expandOnlyOnTwistieClick; } private readonly _onDidUpdateOptions = new Emitter>(); readonly onDidUpdateOptions: Event> = this._onDidUpdateOptions.event; diff --git a/src/vs/base/browser/ui/tree/media/tree.css b/src/vs/base/browser/ui/tree/media/tree.css index 06f775d6246..2701b559d5e 100644 --- a/src/vs/base/browser/ui/tree/media/tree.css +++ b/src/vs/base/browser/ui/tree/media/tree.css @@ -56,6 +56,10 @@ overflow: hidden; } +.monaco-tl-twistie::before { + border-radius: 20px; +} + .monaco-tl-twistie.collapsed::before { transform: rotate(-90deg); } diff --git a/src/vs/base/common/types.ts b/src/vs/base/common/types.ts index da93a17e08a..a91a3cfd174 100644 --- a/src/vs/base/common/types.ts +++ b/src/vs/base/common/types.ts @@ -8,7 +8,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; /** * @returns whether the provided parameter is a JavaScript Array or not. */ -export function isArray(array: T | {}): array is T extends readonly any[] ? (unknown extends T ? never : readonly any[]) : any[] { +export function isArray(array: any): array is any[] { return Array.isArray(array); } diff --git a/src/vs/base/node/languagePacks.js b/src/vs/base/node/languagePacks.js index f47191157c1..5c14fade878 100644 --- a/src/vs/base/node/languagePacks.js +++ b/src/vs/base/node/languagePacks.js @@ -3,256 +3,257 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/// + //@ts-check -'use strict'; - -/** - * @param {NodeRequire} nodeRequire - * @param {typeof import('path')} path - * @param {typeof import('fs')} fs - * @param {typeof import('../common/performance')} perf - */ -function factory(nodeRequire, path, fs, perf) { +(function () { + 'use strict'; /** - * @param {string} file - * @returns {Promise} + * @param {NodeRequire} nodeRequire + * @param {typeof import('path')} path + * @param {typeof import('fs')} fs + * @param {typeof import('../common/performance')} perf */ - function exists(file) { - return new Promise(c => fs.exists(file, c)); - } + function factory(nodeRequire, path, fs, perf) { - /** - * @param {string} file - * @returns {Promise} - */ - function touch(file) { - return new Promise((c, e) => { const d = new Date(); fs.utimes(file, d, d, err => err ? e(err) : c()); }); - } - - /** - * @param {string} dir - * @returns {Promise} - */ - function mkdirp(dir) { - return new Promise((c, e) => fs.mkdir(dir, { recursive: true }, err => (err && err.code !== 'EEXIST') ? e(err) : c(dir))); - } - - /** - * @param {string} location - * @returns {Promise} - */ - function rimraf(location) { - return new Promise((c, e) => fs.rmdir(location, { recursive: true }, err => (err && err.code !== 'ENOENT') ? e(err) : c())); - } - - /** - * @param {string} file - * @returns {Promise} - */ - function readFile(file) { - return new Promise((c, e) => fs.readFile(file, 'utf8', (err, data) => err ? e(err) : c(data))); - } - - /** - * @param {string} file - * @param {string} content - * @returns {Promise} - */ - function writeFile(file, content) { - return new Promise((c, e) => fs.writeFile(file, content, 'utf8', err => err ? e(err) : c())); - } - - /** - * @param {string} userDataPath - * @returns {object} - */ - function getLanguagePackConfigurations(userDataPath) { - const configFile = path.join(userDataPath, 'languagepacks.json'); - try { - return nodeRequire(configFile); - } catch (err) { - // Do nothing. If we can't read the file we have no - // language pack config. + /** + * @param {string} file + * @returns {Promise} + */ + function exists(file) { + return new Promise(c => fs.exists(file, c)); } - return undefined; - } - /** - * @param {object} config - * @param {string} locale - */ - function resolveLanguagePackLocale(config, locale) { - try { - while (locale) { - if (config[locale]) { - return locale; - } else { - const index = locale.lastIndexOf('-'); - if (index > 0) { - locale = locale.substring(0, index); + /** + * @param {string} file + * @returns {Promise} + */ + function touch(file) { + return new Promise((c, e) => { const d = new Date(); fs.utimes(file, d, d, err => err ? e(err) : c()); }); + } + + /** + * @param {string} dir + * @returns {Promise} + */ + function mkdirp(dir) { + return new Promise((c, e) => fs.mkdir(dir, { recursive: true }, err => (err && err.code !== 'EEXIST') ? e(err) : c(dir))); + } + + /** + * @param {string} location + * @returns {Promise} + */ + function rimraf(location) { + return new Promise((c, e) => fs.rmdir(location, { recursive: true }, err => (err && err.code !== 'ENOENT') ? e(err) : c())); + } + + /** + * @param {string} file + * @returns {Promise} + */ + function readFile(file) { + return new Promise((c, e) => fs.readFile(file, 'utf8', (err, data) => err ? e(err) : c(data))); + } + + /** + * @param {string} file + * @param {string} content + * @returns {Promise} + */ + function writeFile(file, content) { + return new Promise((c, e) => fs.writeFile(file, content, 'utf8', err => err ? e(err) : c())); + } + + /** + * @param {string} userDataPath + * @returns {object} + */ + function getLanguagePackConfigurations(userDataPath) { + const configFile = path.join(userDataPath, 'languagepacks.json'); + try { + return nodeRequire(configFile); + } catch (err) { + // Do nothing. If we can't read the file we have no + // language pack config. + } + return undefined; + } + + /** + * @param {object} config + * @param {string} locale + */ + function resolveLanguagePackLocale(config, locale) { + try { + while (locale) { + if (config[locale]) { + return locale; } else { - return undefined; + const index = locale.lastIndexOf('-'); + if (index > 0) { + locale = locale.substring(0, index); + } else { + return undefined; + } } } + } catch (err) { + console.error('Resolving language pack configuration failed.', err); } - } catch (err) { - console.error('Resolving language pack configuration failed.', err); - } - return undefined; - } - - /** - * @param {string} commit - * @param {string} userDataPath - * @param {string} metaDataFile - * @param {string} locale - */ - function getNLSConfiguration(commit, userDataPath, metaDataFile, locale) { - if (locale === 'pseudo') { - return Promise.resolve({ locale: locale, availableLanguages: {}, pseudo: true }); + return undefined; } - if (process.env['VSCODE_DEV']) { - return Promise.resolve({ locale: locale, availableLanguages: {} }); - } - - // We have a built version so we have extracted nls file. Try to find - // the right file to use. - - // Check if we have an English or English US locale. If so fall to default since that is our - // English translation (we don't ship *.nls.en.json files) - if (locale && (locale === 'en' || locale === 'en-us')) { - return Promise.resolve({ locale: locale, availableLanguages: {} }); - } - - const initialLocale = locale; - - perf.mark('code/willGenerateNls'); - - const defaultResult = function (locale) { - perf.mark('code/didGenerateNls'); - return Promise.resolve({ locale: locale, availableLanguages: {} }); - }; - try { - if (!commit) { - return defaultResult(initialLocale); + /** + * @param {string} commit + * @param {string} userDataPath + * @param {string} metaDataFile + * @param {string} locale + */ + function getNLSConfiguration(commit, userDataPath, metaDataFile, locale) { + if (locale === 'pseudo') { + return Promise.resolve({ locale: locale, availableLanguages: {}, pseudo: true }); } - const configs = getLanguagePackConfigurations(userDataPath); - if (!configs) { - return defaultResult(initialLocale); + + if (process.env['VSCODE_DEV']) { + return Promise.resolve({ locale: locale, availableLanguages: {} }); } - locale = resolveLanguagePackLocale(configs, locale); - if (!locale) { - return defaultResult(initialLocale); + + // We have a built version so we have extracted nls file. Try to find + // the right file to use. + + // Check if we have an English or English US locale. If so fall to default since that is our + // English translation (we don't ship *.nls.en.json files) + if (locale && (locale === 'en' || locale === 'en-us')) { + return Promise.resolve({ locale: locale, availableLanguages: {} }); } - const packConfig = configs[locale]; - let mainPack; - if (!packConfig || typeof packConfig.hash !== 'string' || !packConfig.translations || typeof (mainPack = packConfig.translations['vscode']) !== 'string') { - return defaultResult(initialLocale); - } - return exists(mainPack).then(fileExists => { - if (!fileExists) { + + const initialLocale = locale; + + perf.mark('code/willGenerateNls'); + + const defaultResult = function (locale) { + perf.mark('code/didGenerateNls'); + return Promise.resolve({ locale: locale, availableLanguages: {} }); + }; + try { + if (!commit) { return defaultResult(initialLocale); } - const packId = packConfig.hash + '.' + locale; - const cacheRoot = path.join(userDataPath, 'clp', packId); - const coreLocation = path.join(cacheRoot, commit); - const translationsConfigFile = path.join(cacheRoot, 'tcf.json'); - const corruptedFile = path.join(cacheRoot, 'corrupted.info'); - const result = { - locale: initialLocale, - availableLanguages: { '*': locale }, - _languagePackId: packId, - _translationsConfigFile: translationsConfigFile, - _cacheRoot: cacheRoot, - _resolvedLanguagePackCoreLocation: coreLocation, - _corruptedFile: corruptedFile - }; - return exists(corruptedFile).then(corrupted => { - // The nls cache directory is corrupted. - let toDelete; - if (corrupted) { - toDelete = rimraf(cacheRoot); - } else { - toDelete = Promise.resolve(undefined); + const configs = getLanguagePackConfigurations(userDataPath); + if (!configs) { + return defaultResult(initialLocale); + } + locale = resolveLanguagePackLocale(configs, locale); + if (!locale) { + return defaultResult(initialLocale); + } + const packConfig = configs[locale]; + let mainPack; + if (!packConfig || typeof packConfig.hash !== 'string' || !packConfig.translations || typeof (mainPack = packConfig.translations['vscode']) !== 'string') { + return defaultResult(initialLocale); + } + return exists(mainPack).then(fileExists => { + if (!fileExists) { + return defaultResult(initialLocale); } - return toDelete.then(() => { - return exists(coreLocation).then(fileExists => { - if (fileExists) { - // We don't wait for this. No big harm if we can't touch - touch(coreLocation).catch(() => { }); - perf.mark('code/didGenerateNls'); - return result; - } - return mkdirp(coreLocation).then(() => { - return Promise.all([readFile(metaDataFile), readFile(mainPack)]); - }).then(values => { - const metadata = JSON.parse(values[0]); - const packData = JSON.parse(values[1]).contents; - const bundles = Object.keys(metadata.bundles); - const writes = []; - for (const bundle of bundles) { - const modules = metadata.bundles[bundle]; - const target = Object.create(null); - for (const module of modules) { - const keys = metadata.keys[module]; - const defaultMessages = metadata.messages[module]; - const translations = packData[module]; - let targetStrings; - if (translations) { - targetStrings = []; - for (let i = 0; i < keys.length; i++) { - const elem = keys[i]; - const key = typeof elem === 'string' ? elem : elem.key; - let translatedMessage = translations[key]; - if (translatedMessage === undefined) { - translatedMessage = defaultMessages[i]; - } - targetStrings.push(translatedMessage); - } - } else { - targetStrings = defaultMessages; - } - target[module] = targetStrings; - } - writes.push(writeFile(path.join(coreLocation, bundle.replace(/\//g, '!') + '.nls.json'), JSON.stringify(target))); + const packId = packConfig.hash + '.' + locale; + const cacheRoot = path.join(userDataPath, 'clp', packId); + const coreLocation = path.join(cacheRoot, commit); + const translationsConfigFile = path.join(cacheRoot, 'tcf.json'); + const corruptedFile = path.join(cacheRoot, 'corrupted.info'); + const result = { + locale: initialLocale, + availableLanguages: { '*': locale }, + _languagePackId: packId, + _translationsConfigFile: translationsConfigFile, + _cacheRoot: cacheRoot, + _resolvedLanguagePackCoreLocation: coreLocation, + _corruptedFile: corruptedFile + }; + return exists(corruptedFile).then(corrupted => { + // The nls cache directory is corrupted. + let toDelete; + if (corrupted) { + toDelete = rimraf(cacheRoot); + } else { + toDelete = Promise.resolve(undefined); + } + return toDelete.then(() => { + return exists(coreLocation).then(fileExists => { + if (fileExists) { + // We don't wait for this. No big harm if we can't touch + touch(coreLocation).catch(() => { }); + perf.mark('code/didGenerateNls'); + return result; } - writes.push(writeFile(translationsConfigFile, JSON.stringify(packConfig.translations))); - return Promise.all(writes); - }).then(() => { - perf.mark('code/didGenerateNls'); - return result; - }).catch(err => { - console.error('Generating translation files failed.', err); - return defaultResult(locale); + return mkdirp(coreLocation).then(() => { + return Promise.all([readFile(metaDataFile), readFile(mainPack)]); + }).then(values => { + const metadata = JSON.parse(values[0]); + const packData = JSON.parse(values[1]).contents; + const bundles = Object.keys(metadata.bundles); + const writes = []; + for (const bundle of bundles) { + const modules = metadata.bundles[bundle]; + const target = Object.create(null); + for (const module of modules) { + const keys = metadata.keys[module]; + const defaultMessages = metadata.messages[module]; + const translations = packData[module]; + let targetStrings; + if (translations) { + targetStrings = []; + for (let i = 0; i < keys.length; i++) { + const elem = keys[i]; + const key = typeof elem === 'string' ? elem : elem.key; + let translatedMessage = translations[key]; + if (translatedMessage === undefined) { + translatedMessage = defaultMessages[i]; + } + targetStrings.push(translatedMessage); + } + } else { + targetStrings = defaultMessages; + } + target[module] = targetStrings; + } + writes.push(writeFile(path.join(coreLocation, bundle.replace(/\//g, '!') + '.nls.json'), JSON.stringify(target))); + } + writes.push(writeFile(translationsConfigFile, JSON.stringify(packConfig.translations))); + return Promise.all(writes); + }).then(() => { + perf.mark('code/didGenerateNls'); + return result; + }).catch(err => { + console.error('Generating translation files failed.', err); + return defaultResult(locale); + }); }); }); }); }); - }); - } catch (err) { - console.error('Generating translation files failed.', err); - return defaultResult(locale); + } catch (err) { + console.error('Generating translation files failed.', err); + return defaultResult(locale); + } } + + return { + getNLSConfiguration + }; } - return { - getNLSConfiguration - }; -} - - -// @ts-ignore -if (typeof define === 'function') { - // amd - // @ts-ignore - define(['path', 'fs', 'vs/base/common/performance'], function (path, fs, perf) { return factory(require.__$__nodeRequire, path, fs, perf); }); -} else if (typeof module === 'object' && typeof module.exports === 'object') { - const path = require('path'); - const fs = require('fs'); - const perf = require('../common/performance'); - module.exports = factory(require, path, fs, perf); -} else { - throw new Error('Unknown context'); -} + if (typeof define === 'function') { + // amd + define(['require', 'path', 'fs', 'vs/base/common/performance'], function (require, /** @type {typeof import('path')} */ path, /** @type {typeof import('fs')} */ fs, /** @type {typeof import('../common/performance')} */ perf) { return factory(require.__$__nodeRequire, path, fs, perf); }); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + const path = require('path'); + const fs = require('fs'); + const perf = require('../common/performance'); + module.exports = factory(require, path, fs, perf); + } else { + throw new Error('Unknown context'); + } +}()); diff --git a/src/vs/base/node/userDataPath.d.ts b/src/vs/base/node/userDataPath.d.ts new file mode 100644 index 00000000000..bc09b834a7e --- /dev/null +++ b/src/vs/base/node/userDataPath.d.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Returns the user data path to use. + */ +export function getDefaultUserDataPath(): string; diff --git a/src/vs/base/node/userDataPath.js b/src/vs/base/node/userDataPath.js new file mode 100644 index 00000000000..93384cb8f4e --- /dev/null +++ b/src/vs/base/node/userDataPath.js @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * 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 () { + 'use strict'; + + /** + * @param {typeof import('path')} path + * @param {typeof import('os')} os + * @param {string} productName + */ + function factory(path, os, productName) { + + function getDefaultUserDataPath() { + + // Support global VSCODE_APPDATA environment variable + let appDataPath = process.env['VSCODE_APPDATA']; + + // Otherwise check per platform + if (!appDataPath) { + switch (process.platform) { + case 'win32': + appDataPath = process.env['APPDATA']; + if (!appDataPath) { + const userProfile = process.env['USERPROFILE']; + if (typeof userProfile !== 'string') { + throw new Error('Windows: Unexpected undefined %USERPROFILE% environment variable'); + } + appDataPath = path.join(userProfile, 'AppData', 'Roaming'); + } + break; + case 'darwin': + appDataPath = path.join(os.homedir(), 'Library', 'Application Support'); + break; + case 'linux': + appDataPath = process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config'); + break; + default: + throw new Error('Platform not supported'); + } + } + + return path.join(appDataPath, productName); + } + + return { + getDefaultUserDataPath + }; + } + + if (typeof define === 'function') { + define(['require', 'path', 'os', 'vs/base/common/network', 'vs/base/common/resources'], function (require, /** @type {typeof import('path')} */ path, /** @type {typeof import('os')} */ os, /** @type {typeof import('../common/network')} */ network, /** @type {typeof import("../common/resources")} */ resources) { + const rootPath = resources.dirname(network.FileAccess.asFileUri('', require)); + const pkg = require.__$__nodeRequire(resources.joinPath(rootPath, 'package.json').fsPath); + + return factory(path, os, pkg.name); + }); // amd + } else if (typeof module === 'object' && typeof module.exports === 'object') { + const pkg = require('../../../../package.json'); + const path = require('path'); + const os = require('os'); + + module.exports = factory(path, os, pkg.name); // commonjs + } else { + throw new Error('Unknown context'); + } +}()); diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index 038816012f3..2ab5e7a478d 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -56,7 +56,7 @@ export interface IQuickInputStyles { countBadge: ICountBadgetyles; button: IButtonStyles; progressBar: IProgressBarStyles; - list: IListStyles & { listInactiveFocusForeground?: Color; pickerGroupBorder?: Color; pickerGroupForeground?: Color; }; + list: IListStyles & { pickerGroupBorder?: Color; pickerGroupForeground?: Color; }; } export interface IQuickInputWidgetStyles { @@ -1706,10 +1706,6 @@ export class QuickInputController extends Disposable { this.ui.list.style(this.styles.list); const content: string[] = []; - if (this.styles.list.listInactiveFocusForeground) { - content.push(`.monaco-list .monaco-list-row.focused { color: ${this.styles.list.listInactiveFocusForeground}; }`); - content.push(`.monaco-list .monaco-list-row.focused:hover { color: ${this.styles.list.listInactiveFocusForeground}; }`); // overwrite :hover style in this case! - } if (this.styles.list.pickerGroupBorder) { content.push(`.quick-input-list .quick-input-list-entry { border-top-color: ${this.styles.list.pickerGroupBorder}; }`); } diff --git a/src/vs/base/parts/storage/common/storage.ts b/src/vs/base/parts/storage/common/storage.ts index c2f5ff5a5cd..66023e7d651 100644 --- a/src/vs/base/parts/storage/common/storage.ts +++ b/src/vs/base/parts/storage/common/storage.ts @@ -89,6 +89,8 @@ export class Storage extends Disposable implements IStorage { private pendingDeletes = new Set(); private pendingInserts = new Map(); + private pendingClose: Promise | undefined = undefined; + private readonly whenFlushedCallbacks: Function[] = []; constructor( @@ -256,10 +258,15 @@ export class Storage extends Disposable implements IStorage { } async close(): Promise { - if (this.state === StorageState.Closed) { - return; // return if already closed + if (!this.pendingClose) { + this.pendingClose = this.doClose(); } + return this.pendingClose; + } + + private async doClose(): Promise { + // Update state this.state = StorageState.Closed; diff --git a/src/vs/base/node/paths.ts b/src/vs/base/test/node/userDataPath.test.ts similarity index 56% rename from src/vs/base/node/paths.ts rename to src/vs/base/test/node/userDataPath.test.ts index eaf03e6e400..74d0ae4f26e 100644 --- a/src/vs/base/node/paths.ts +++ b/src/vs/base/test/node/userDataPath.test.ts @@ -3,9 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { FileAccess } from 'vs/base/common/network'; +import * as assert from 'assert'; +import { getDefaultUserDataPath } from 'vs/base/node/userDataPath'; -const pathsPath = FileAccess.asFileUri('paths', require).fsPath; -const paths = require.__$__nodeRequire<{ getDefaultUserDataPath(): string }>(pathsPath); +suite('User data path', () => { -export const getDefaultUserDataPath = paths.getDefaultUserDataPath; + test('getDefaultUserDataPath', () => { + const path = getDefaultUserDataPath(); + assert.ok(path.length > 0); + }); +}); diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcess.js b/src/vs/code/electron-browser/sharedProcess/sharedProcess.js index 2bb0a0bb730..50d8d3f7f8f 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcess.js +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcess.js @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ //@ts-check -'use strict'; - (function () { + 'use strict'; + const bootstrap = bootstrapLib(); const bootstrapWindow = bootstrapWindowLib(); @@ -38,5 +38,4 @@ } //#endregion - }()); diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 8081fa4f3de..4ebb2bf0fec 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -58,8 +58,7 @@ import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; import { UserDataAutoSyncService } from 'vs/platform/userDataSync/electron-sandbox/userDataAutoSyncService'; -import { NativeStorageService } from 'vs/platform/storage/node/storageService'; -import { GlobalStorageDatabaseChannelClient } from 'vs/platform/storage/node/storageIpc'; +import { NativeStorageService2 } from 'vs/platform/storage/electron-sandbox/storageService2'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; import { UserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSyncResourceEnablementService'; @@ -175,8 +174,8 @@ class SharedProcessMain extends Disposable { await configurationService.initialize(); - // Storage - const storageService = new NativeStorageService(new GlobalStorageDatabaseChannelClient(mainProcessService.getChannel('storage')), logService, environmentService); + // Storage (global access only) + const storageService = new NativeStorageService2(undefined, mainProcessService, environmentService); services.set(IStorageService, storageService); await storageService.initialize(); diff --git a/src/vs/code/electron-browser/workbench/workbench.js b/src/vs/code/electron-browser/workbench/workbench.js index 64082146a6d..0c52f3cda25 100644 --- a/src/vs/code/electron-browser/workbench/workbench.js +++ b/src/vs/code/electron-browser/workbench/workbench.js @@ -6,9 +6,9 @@ /// //@ts-check -'use strict'; - (function () { + 'use strict'; + const bootstrapWindow = bootstrapWindowLib(); // Add a perf entry right from the top @@ -179,5 +179,4 @@ } //#endregion - }()); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index ef4909c7d6b..1a378fd011f 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -58,8 +58,8 @@ import { joinPath } from 'vs/base/common/resources'; import { localize } from 'vs/nls'; import { Schemas } from 'vs/base/common/network'; import { SnapUpdateService } from 'vs/platform/update/electron-main/updateService.snap'; -import { IStorageMainService, StorageMainService } from 'vs/platform/storage/node/storageMainService'; -import { GlobalStorageDatabaseChannel } from 'vs/platform/storage/node/storageIpc'; +import { IStorageMainService, StorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; +import { StorageDatabaseChannel } from 'vs/platform/storage/electron-main/storageIpc'; import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainService'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; import { WorkspacesHistoryMainService, IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; @@ -102,7 +102,7 @@ export class CodeApplication extends Disposable { private readonly userEnv: IProcessEnvironment, @IInstantiationService private readonly mainInstantiationService: IInstantiationService, @ILogService private readonly logService: ILogService, - @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, + @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @IConfigurationService private readonly configurationService: IConfigurationService, @IStateService private readonly stateService: IStateService, @@ -168,7 +168,7 @@ export class CodeApplication extends Disposable { event.preventDefault(); }); app.on('remote-get-current-web-contents', event => { - if (this.environmentService.args.driver) { + if (this.environmentMainService.args.driver) { return; // the driver needs access to web contents } @@ -190,7 +190,7 @@ export class CodeApplication extends Disposable { } const srcUri = uri.fsPath.toLowerCase(); - const rootUri = URI.file(this.environmentService.appRoot).fsPath.toLowerCase(); + const rootUri = URI.file(this.environmentMainService.appRoot).fsPath.toLowerCase(); return srcUri.startsWith(rootUri + sep); }; @@ -255,7 +255,7 @@ export class CodeApplication extends Disposable { runningTimeout = setTimeout(() => { this.windowsMainService?.open({ context: OpenContext.DOCK /* can also be opening from finder while app is running */, - cli: this.environmentService.args, + cli: this.environmentMainService.args, urisToOpen: macOpenFileURIs, gotoLineMode: false, preferNewWindow: true /* dropping on the dock or opening from finder prefers to open in a new window */ @@ -328,7 +328,7 @@ export class CodeApplication extends Disposable { args = window.config; env = { ...process.env, ...window.config.userEnv }; } else { - args = this.environmentService.args; + args = this.environmentMainService.args; env = process.env; } @@ -376,7 +376,7 @@ export class CodeApplication extends Disposable { } } - if (typeof path !== 'string' || !isAbsolute(path) || !isEqualOrParent(path, this.environmentService.cachedLanguagesPath, !isLinux)) { + if (typeof path !== 'string' || !isAbsolute(path) || !isEqualOrParent(path, this.environmentMainService.cachedLanguagesPath, !isLinux)) { return undefined; } @@ -404,8 +404,8 @@ export class CodeApplication extends Disposable { async startup(): Promise { this.logService.debug('Starting VS Code'); - this.logService.debug(`from: ${this.environmentService.appRoot}`); - this.logService.debug('args:', this.environmentService.args); + this.logService.debug(`from: ${this.environmentMainService.appRoot}`); + this.logService.debug('args:', this.environmentMainService.args); // Make sure we associate the program with the app user model id // This will help Windows to associate the running program with @@ -448,10 +448,10 @@ export class CodeApplication extends Disposable { const appInstantiationService = await this.initServices(machineId, sharedProcess, sharedProcessReady); // Create driver - if (this.environmentService.driverHandle) { - const server = await serveDriver(mainProcessElectronServer, this.environmentService.driverHandle, this.environmentService, appInstantiationService); + if (this.environmentMainService.driverHandle) { + const server = await serveDriver(mainProcessElectronServer, this.environmentMainService.driverHandle, this.environmentMainService, appInstantiationService); - this.logService.info('Driver started at:', this.environmentService.driverHandle); + this.logService.info('Driver started at:', this.environmentMainService.driverHandle); this._register(server); } @@ -468,7 +468,7 @@ export class CodeApplication extends Disposable { appInstantiationService.invokeFunction(accessor => this.afterWindowOpen(accessor)); // Tracing: Stop tracing after windows are ready if enabled - if (this.environmentService.args.trace) { + if (this.environmentMainService.args.trace) { appInstantiationService.invokeFunction(accessor => this.stopTracingEventually(accessor, windows)); } } @@ -509,7 +509,7 @@ export class CodeApplication extends Disposable { // Spawn shared process after the first window has opened and 3s have passed this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => { this._register(new RunOnceScheduler(async () => { - sharedProcess.spawn(await resolveShellEnv(this.logService, this.environmentService.args, process.env)); + sharedProcess.spawn(await resolveShellEnv(this.logService, this.environmentMainService.args, process.env)); }, 3000)).schedule(); }); @@ -580,23 +580,21 @@ export class CodeApplication extends Disposable { services.set(IExtensionUrlTrustService, new SyncDescriptor(ExtensionUrlTrustService)); // Storage - const storageMainService = new StorageMainService(this.logService, this.environmentService); - services.set(IStorageMainService, storageMainService); - this.lifecycleMainService.onWillShutdown(e => e.join(storageMainService.close())); + services.set(IStorageMainService, new SyncDescriptor(StorageMainService)); // Backups - const backupMainService = new BackupMainService(this.environmentService, this.configurationService, this.logService); + const backupMainService = new BackupMainService(this.environmentMainService, this.configurationService, this.logService); services.set(IBackupMainService, backupMainService); // URL handling services.set(IURLService, new SyncDescriptor(NativeURLService)); // Telemetry - if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { + if (!this.environmentMainService.isExtensionDevelopment && !this.environmentMainService.args['disable-telemetry'] && !!product.enableTelemetry) { const channel = getDelayedChannel(sharedProcessReady.then(client => client.getChannel('telemetryAppender'))); const appender = new TelemetryAppenderClient(channel); - const commonProperties = resolveCommonProperties(this.fileService, release(), process.arch, product.commit, product.version, machineId, product.msftInternalDomains, this.environmentService.installSourcePath); - const piiPaths = [this.environmentService.appRoot, this.environmentService.extensionsPath]; + const commonProperties = resolveCommonProperties(this.fileService, release(), process.arch, product.commit, product.version, machineId, product.msftInternalDomains, this.environmentMainService.installSourcePath); + const piiPaths = [this.environmentMainService.appRoot, this.environmentMainService.extensionsPath]; const config: ITelemetryServiceConfig = { appender, commonProperties, piiPaths, sendErrorTelemetry: true }; services.set(ITelemetryService, new SyncDescriptor(TelemetryService, [config])); @@ -666,7 +664,7 @@ export class CodeApplication extends Disposable { mainProcessElectronServer.registerChannel('webview', webviewChannel); // Storage (main & shared process) - const storageChannel = this._register(new GlobalStorageDatabaseChannel(this.logService, accessor.get(IStorageMainService))); + const storageChannel = this._register(new StorageDatabaseChannel(this.logService, accessor.get(IStorageMainService))); mainProcessElectronServer.registerChannel('storage', storageChannel); sharedProcessClient.then(client => client.registerChannel('storage', storageChannel)); @@ -699,7 +697,7 @@ export class CodeApplication extends Disposable { const pendingWindowOpenablesFromProtocolLinks: IWindowOpenable[] = []; const pendingProtocolLinksToHandle = [ // Windows/Linux: protocol handler invokes CLI with --open-url - ...this.environmentService.args['open-url'] ? this.environmentService.args._urls || [] : [], + ...this.environmentMainService.args['open-url'] ? this.environmentMainService.args._urls || [] : [], // macOS: open-url events ...((global).getOpenUrls() || []) as string[] @@ -736,7 +734,7 @@ export class CodeApplication extends Disposable { // or open new windows. The URL handler will be invoked from // protocol invocations outside of VSCode. const app = this; - const environmentService = this.environmentService; + const environmentService = this.environmentMainService; urlService.registerHandler({ async handleURL(uri: URI, options?: IOpenURLOptions): Promise { @@ -791,10 +789,10 @@ export class CodeApplication extends Disposable { urlService.registerHandler(new URLHandlerChannelClient(urlHandlerChannel)); // Watch Electron URLs and forward them to the UrlService - this._register(new ElectronURLListener(pendingProtocolLinksToHandle, urlService, windowsMainService, this.environmentService)); + this._register(new ElectronURLListener(pendingProtocolLinksToHandle, urlService, windowsMainService, this.environmentMainService)); // Open our first window - const args = this.environmentService.args; + const args = this.environmentMainService.args; const macOpenFiles: string[] = (global).macOpenFiles; const context = isLaunchedFromCli(process.env) ? OpenContext.CLI : OpenContext.DESKTOP; const hasCliArgs = args._.length; @@ -864,7 +862,7 @@ export class CodeApplication extends Disposable { mnemonicButtonLabel(localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&No")), ], cancelId: 1, - message: localize('confirmOpenMessage', "An external application wants to open '{0}' in {1}. Do you want to open this file or folder?", getPathLabel(uri.fsPath, this.environmentService), product.nameShort), + message: localize('confirmOpenMessage', "An external application wants to open '{0}' in {1}. Do you want to open this file or folder?", getPathLabel(uri.fsPath, this.environmentMainService), product.nameShort), detail: localize('confirmOpenDetail', "If you did not initiate this request, it may represent an attempted attack on your system. Unless you took an explicit action to initiate this request, you should press 'No'"), noLink: true }); @@ -960,13 +958,13 @@ export class CodeApplication extends Disposable { } // Start to fetch shell environment (if needed) after window has opened - resolveShellEnv(this.logService, this.environmentService.args, process.env); + resolveShellEnv(this.logService, this.environmentMainService.args, process.env); // If enable-crash-reporter argv is undefined then this is a fresh start, // based on telemetry.enableCrashreporter settings, generate a UUID which // will be used as crash reporter id and also update the json file. try { - const argvContent = await this.fileService.readFile(this.environmentService.argvResource); + const argvContent = await this.fileService.readFile(this.environmentMainService.argvResource); const argvString = argvContent.value.toString(); const argvJSON = JSON.parse(stripComments(argvString)); if (argvJSON['enable-crash-reporter'] === undefined) { @@ -984,7 +982,7 @@ export class CodeApplication extends Disposable { ]; const newArgvString = argvString.substring(0, argvString.length - 2).concat(',\n', additionalArgvContent.join('\n')); - await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(newArgvString)); + await this.fileService.writeFile(this.environmentMainService.argvResource, VSBuffer.fromString(newArgvString)); } } catch (error) { this.logService.error(error); @@ -1004,7 +1002,7 @@ export class CodeApplication extends Disposable { recordingStopped = true; // only once - const path = await contentTracing.stopRecording(joinPath(this.environmentService.userHome, `${product.applicationName}-${Math.random().toString(16).slice(-4)}.trace.txt`).fsPath); + const path = await contentTracing.stopRecording(joinPath(this.environmentMainService.userHome, `${product.applicationName}-${Math.random().toString(16).slice(-4)}.trace.txt`).fsPath); if (!timeout) { dialogMainService.showMessageBox({ diff --git a/src/vs/code/electron-main/auth.ts b/src/vs/code/electron-main/auth.ts index b9669f983c7..70df692cf81 100644 --- a/src/vs/code/electron-main/auth.ts +++ b/src/vs/code/electron-main/auth.ts @@ -201,7 +201,7 @@ export class ProxyAuthHandler extends Disposable { const proxyAuthResponseHandler = async (event: ElectronEvent, channel: string, reply: Credentials & { remember: boolean } | undefined /* canceled */) => { if (channel === payload.replyChannel) { this.logService.trace(`auth#doResolveProxyCredentials - exit - received credentials from window ${window.id}`); - window.win.webContents.off('ipc-message', proxyAuthResponseHandler); + window.win?.webContents.off('ipc-message', proxyAuthResponseHandler); // We got credentials from the window if (reply) { @@ -229,7 +229,7 @@ export class ProxyAuthHandler extends Disposable { } }; - window.win.webContents.on('ipc-message', proxyAuthResponseHandler); + window.win?.webContents.on('ipc-message', proxyAuthResponseHandler); }); // Remember credentials for the session in case diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 0ccc3092bd4..7cbdd078f09 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -181,9 +181,9 @@ class CodeMain { return [new InstantiationService(services, true), instanceEnvironment, environmentService, configurationService, stateService, bufferLogService]; } - private patchEnvironment(environmentService: IEnvironmentMainService): IProcessEnvironment { + private patchEnvironment(environmentMainService: IEnvironmentMainService): IProcessEnvironment { const instanceEnvironment: IProcessEnvironment = { - VSCODE_IPC_HOOK: environmentService.mainIPCHandle + VSCODE_IPC_HOOK: environmentMainService.mainIPCHandle }; ['VSCODE_NLS_CONFIG', 'VSCODE_PORTABLE'].forEach(key => { @@ -198,16 +198,16 @@ class CodeMain { return instanceEnvironment; } - private initServices(environmentService: IEnvironmentMainService, configurationService: ConfigurationService, stateService: StateService): Promise { + private initServices(environmentMainService: IEnvironmentMainService, configurationService: ConfigurationService, stateService: StateService): Promise { // Environment service (paths) const environmentServiceInitialization = Promise.all([ - environmentService.extensionsPath, - environmentService.nodeCachedDataDir, - environmentService.logsPath, - environmentService.globalStorageHome.fsPath, - environmentService.workspaceStorageHome.fsPath, - environmentService.backupHome + environmentMainService.extensionsPath, + environmentMainService.nodeCachedDataDir, + environmentMainService.logsPath, + environmentMainService.globalStorageHome.fsPath, + environmentMainService.workspaceStorageHome.fsPath, + environmentMainService.backupHome ].map(path => path ? promises.mkdir(path, { recursive: true }) : undefined)); // Configuration service @@ -219,14 +219,14 @@ class CodeMain { return Promise.all([environmentServiceInitialization, configurationServiceInitialization, stateServiceInitialization]); } - private async doStartup(args: NativeParsedArgs, logService: ILogService, environmentService: IEnvironmentMainService, lifecycleMainService: ILifecycleMainService, instantiationService: IInstantiationService, retry: boolean): Promise { + private async doStartup(args: NativeParsedArgs, logService: ILogService, environmentMainService: IEnvironmentMainService, lifecycleMainService: ILifecycleMainService, instantiationService: IInstantiationService, retry: boolean): Promise { // Try to setup a server for running. If that succeeds it means // we are the first instance to startup. Otherwise it is likely // that another instance is already running. let mainProcessNodeIpcServer: NodeIPCServer; try { - mainProcessNodeIpcServer = await nodeIPCServe(environmentService.mainIPCHandle); + mainProcessNodeIpcServer = await nodeIPCServe(environmentMainService.mainIPCHandle); once(lifecycleMainService.onWillShutdown)(() => mainProcessNodeIpcServer.dispose()); } catch (error) { @@ -235,7 +235,7 @@ class CodeMain { if (error.code !== 'EADDRINUSE') { // Show a dialog for errors that can be resolved by the user - this.handleStartupDataDirError(environmentService, error); + this.handleStartupDataDirError(environmentMainService, error); // Any other runtime error is just printed to the console throw error; @@ -244,7 +244,7 @@ class CodeMain { // there's a running instance, let's connect to it let client: NodeIPCClient; try { - client = await nodeIPCConnect(environmentService.mainIPCHandle, 'main'); + client = await nodeIPCConnect(environmentMainService.mainIPCHandle, 'main'); } catch (error) { // Handle unexpected connection errors by showing a dialog to the user @@ -263,18 +263,18 @@ class CodeMain { // let's delete it, since we can't connect to it and then // retry the whole thing try { - unlinkSync(environmentService.mainIPCHandle); + unlinkSync(environmentMainService.mainIPCHandle); } catch (error) { logService.warn('Could not delete obsolete instance handle', error); throw error; } - return this.doStartup(args, logService, environmentService, lifecycleMainService, instantiationService, false); + return this.doStartup(args, logService, environmentMainService, lifecycleMainService, instantiationService, false); } // Tests from CLI require to be the only instance currently - if (environmentService.extensionTestsLocationURI && !environmentService.debugExtensionHost.break) { + if (environmentMainService.extensionTestsLocationURI && !environmentMainService.debugExtensionHost.break) { const msg = 'Running extension tests from the command line is currently only supported if no other instance of Code is running.'; logService.error(msg); client.dispose(); @@ -344,9 +344,9 @@ class CodeMain { return mainProcessNodeIpcServer; } - private handleStartupDataDirError(environmentService: IEnvironmentMainService, error: NodeJS.ErrnoException): void { + private handleStartupDataDirError(environmentMainService: IEnvironmentMainService, error: NodeJS.ErrnoException): void { if (error.code === 'EACCES' || error.code === 'EPERM') { - const directories = coalesce([environmentService.userDataPath, environmentService.extensionsPath, XDG_RUNTIME_DIR]).map(folder => getPathLabel(folder, environmentService)); + const directories = coalesce([environmentMainService.userDataPath, environmentMainService.extensionsPath, XDG_RUNTIME_DIR]).map(folder => getPathLabel(folder, environmentMainService)); this.showStartupWarningDialog( localize('startupDataDirError', "Unable to write program user data."), @@ -369,9 +369,9 @@ class CodeMain { }); } - private async windowsAllowSetForegroundWindow(launchService: ILaunchMainService, logService: ILogService): Promise { + private async windowsAllowSetForegroundWindow(launchMainService: ILaunchMainService, logService: ILogService): Promise { if (isWindows) { - const processId = await launchService.getMainProcessId(); + const processId = await launchMainService.getMainProcessId(); logService.trace('Sending some foreground love to the running instance:', processId); diff --git a/src/vs/code/electron-main/protocol.ts b/src/vs/code/electron-main/protocol.ts index 20644f2c9b9..3e535155c76 100644 --- a/src/vs/code/electron-main/protocol.ts +++ b/src/vs/code/electron-main/protocol.ts @@ -48,10 +48,10 @@ export class FileProtocolHandler extends Disposable { } injectWindowsMainService(windowsMainService: IWindowsMainService): void { - this._register(windowsMainService.onWindowReady(window => { + this._register(windowsMainService.onDidSignalReadyWindow(window => { if (window.config?.extensionDevelopmentPath || window.config?.extensionTestsPath) { const disposables = new DisposableStore(); - disposables.add(Event.any(window.onClose, window.onDestroy)(() => disposables.dispose())); + disposables.add(Event.any(window.onDidClose, window.onDidDestroy)(() => disposables.dispose())); // Allow access to extension development path if (window.config.extensionDevelopmentPath) { diff --git a/src/vs/code/electron-sandbox/issue/issueReporter.js b/src/vs/code/electron-sandbox/issue/issueReporter.js index 2c3529754b3..a51159e580e 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporter.js +++ b/src/vs/code/electron-sandbox/issue/issueReporter.js @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ //@ts-check -'use strict'; - (function () { + 'use strict'; + const bootstrapWindow = bootstrapWindowLib(); // Load issue reporter into window diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorer.js b/src/vs/code/electron-sandbox/processExplorer/processExplorer.js index 3b84f3acf06..36fb6ee5530 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorer.js +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer.js @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ //@ts-check -'use strict'; - (function () { + 'use strict'; + const bootstrapWindow = bootstrapWindowLib(); // Load process explorer into window diff --git a/src/vs/code/electron-sandbox/workbench/workbench.js b/src/vs/code/electron-sandbox/workbench/workbench.js index c5cb1debb26..1946d7f5e3b 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.js +++ b/src/vs/code/electron-sandbox/workbench/workbench.js @@ -6,9 +6,9 @@ /// //@ts-check -'use strict'; - (function () { + 'use strict'; + const bootstrapWindow = bootstrapWindowLib(); // Add a perf entry right from the top @@ -76,5 +76,4 @@ } //#endregion - }()); diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index ee10d9b8003..7b401019e06 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -118,6 +118,10 @@ class ModesContentComputer implements IHoverComputer { const maxColumn = model.getLineMaxColumn(lineNumber); const lineDecorations = this._editor.getLineDecorations(lineNumber).filter((d) => { + if (d.options.isWholeLine) { + return true; + } + const startColumn = (d.range.startLineNumber === lineNumber) ? d.range.startColumn : 1; const endColumn = (d.range.endLineNumber === lineNumber) ? d.range.endColumn : maxColumn; if (startColumn > hoverRange.startColumn || hoverRange.endColumn > endColumn) { diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 884207922f2..94135e09b63 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -21,7 +21,7 @@ import { Context as SuggestContext, CompletionItem } from './suggest'; import { CompletionModel } from './completionModel'; import { attachListStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, IColorTheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { registerColor, editorWidgetBackground, listFocusBackground, activeContrastBorder, listHighlightForeground, editorForeground, editorWidgetBorder, focusBorder, textLinkForeground, textCodeBlockBackground } from 'vs/platform/theme/common/colorRegistry'; +import { registerColor, editorWidgetBackground, quickInputListFocusBackground, activeContrastBorder, listHighlightForeground, editorForeground, editorWidgetBorder, focusBorder, textLinkForeground, textCodeBlockBackground } from 'vs/platform/theme/common/colorRegistry'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { TimeoutTimer, CancelablePromise, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -40,7 +40,7 @@ import { clamp } from 'vs/base/common/numbers'; export const editorSuggestWidgetBackground = registerColor('editorSuggestWidget.background', { dark: editorWidgetBackground, light: editorWidgetBackground, hc: editorWidgetBackground }, nls.localize('editorSuggestWidgetBackground', 'Background color of the suggest widget.')); export const editorSuggestWidgetBorder = registerColor('editorSuggestWidget.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hc: editorWidgetBorder }, nls.localize('editorSuggestWidgetBorder', 'Border color of the suggest widget.')); export const editorSuggestWidgetForeground = registerColor('editorSuggestWidget.foreground', { dark: editorForeground, light: editorForeground, hc: editorForeground }, nls.localize('editorSuggestWidgetForeground', 'Foreground color of the suggest widget.')); -export const editorSuggestWidgetSelectedBackground = registerColor('editorSuggestWidget.selectedBackground', { dark: listFocusBackground, light: listFocusBackground, hc: listFocusBackground }, nls.localize('editorSuggestWidgetSelectedBackground', 'Background color of the selected entry in the suggest widget.')); +export const editorSuggestWidgetSelectedBackground = registerColor('editorSuggestWidget.selectedBackground', { dark: quickInputListFocusBackground, light: quickInputListFocusBackground, hc: quickInputListFocusBackground }, nls.localize('editorSuggestWidgetSelectedBackground', 'Background color of the selected entry in the suggest widget.')); export const editorSuggestWidgetHighlightForeground = registerColor('editorSuggestWidget.highlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hc: listHighlightForeground }, nls.localize('editorSuggestWidgetHighlightForeground', 'Color of the match highlights in the suggest widget.')); const enum State { diff --git a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts index 37055a85416..a4286aa63af 100644 --- a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts +++ b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts @@ -52,7 +52,7 @@ class TestGlobalStyleSheet extends GlobalStyleSheet { suite('Decoration Render Options', () => { let options: IDecorationRenderOptions = { - gutterIconPath: URI.parse('https://github.com/microsoft/vscode/blob/master/resources/linux/code.png'), + gutterIconPath: URI.parse('https://github.com/microsoft/vscode/blob/main/resources/linux/code.png'), gutterIconSize: 'contain', backgroundColor: 'red', borderColor: 'yellow' @@ -79,7 +79,7 @@ suite('Decoration Render Options', () => { const s = new TestCodeEditorServiceImpl(themeServiceMock, styleSheet); s.registerDecorationType('example', options); const sheet = readStyleSheet(styleSheet); - assert(sheet.indexOf(`{background:url('https://github.com/microsoft/vscode/blob/master/resources/linux/code.png') center center no-repeat;background-size:contain;}`) >= 0); + assert(sheet.indexOf(`{background:url('https://github.com/microsoft/vscode/blob/main/resources/linux/code.png') center center no-repeat;background-size:contain;}`) >= 0); assert(sheet.indexOf(`{background-color:red;border-color:yellow;box-sizing: border-box;}`) >= 0); }); diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 9de9273c464..1e2d2df6c2f 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -321,34 +321,30 @@ export class ExecuteCommandAction extends Action { export class SubmenuItemAction extends SubmenuAction { - readonly item: ISubmenuItem; - constructor( - item: ISubmenuItem, - menuService: IMenuService, - contextKeyService: IContextKeyService, - options?: IMenuActionOptions + readonly item: ISubmenuItem, + private readonly _menuService: IMenuService, + private readonly _contextKeyService: IContextKeyService, + private readonly _options?: IMenuActionOptions ) { + super(`submenuitem.${item.submenu.id}`, typeof item.title === 'string' ? item.title : item.title.value, [], 'submenu'); + } + + get actions(): readonly IAction[] { const result: IAction[] = []; - const menu = menuService.createMenu(item.submenu, contextKeyService); - const groups = menu.getActions(options); + const menu = this._menuService.createMenu(this.item.submenu, this._contextKeyService); + const groups = menu.getActions(this._options); menu.dispose(); - - for (let group of groups) { - const [, actions] = group; - + for (const [, actions] of groups) { if (actions.length > 0) { result.push(...actions); result.push(new Separator()); } } - if (result.length) { result.pop(); // remove last separator } - - super(`submenuitem.${item.submenu.id}`, typeof item.title === 'string' ? item.title : item.title.value, result, 'submenu'); - this.item = item; + return result; } } diff --git a/src/vs/platform/backup/electron-main/backupMainService.ts b/src/vs/platform/backup/electron-main/backupMainService.ts index ac5e6cd2eb1..ac5bb248927 100644 --- a/src/vs/platform/backup/electron-main/backupMainService.ts +++ b/src/vs/platform/backup/electron-main/backupMainService.ts @@ -38,12 +38,12 @@ export class BackupMainService implements IBackupMainService { private readonly backupPathComparer = { isEqual: (pathA: string, pathB: string) => isEqual(pathA, pathB, !isLinux) }; constructor( - @IEnvironmentMainService environmentService: IEnvironmentMainService, + @IEnvironmentMainService environmentMainService: IEnvironmentMainService, @IConfigurationService private readonly configurationService: IConfigurationService, @ILogService private readonly logService: ILogService ) { - this.backupHome = environmentService.backupHome; - this.workspacesJsonPath = environmentService.backupWorkspacesPath; + this.backupHome = environmentMainService.backupHome; + this.workspacesJsonPath = environmentMainService.backupWorkspacesPath; } async initialize(): Promise { diff --git a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts index f0ac255cb2a..9d37b27980d 100644 --- a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts +++ b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts @@ -43,7 +43,12 @@ export class ElectronExtensionHostDebugBroadcastChannel extends Extens return {}; } - const debug = codeWindow.win.webContents.debugger; + const win = codeWindow.win; + if (!win) { + return {}; + } + + const debug = win.webContents.debugger; let listeners = debug.isAttached() ? Infinity : 0; const server = createServer(listener => { @@ -61,7 +66,7 @@ export class ElectronExtensionHostDebugBroadcastChannel extends Extens const onMessage = (_event: Event, method: string, params: unknown, sessionId?: string) => writeMessage(({ method, params, sessionId })); - codeWindow.win.on('close', () => { + win.on('close', () => { debug.removeListener('message', onMessage); listener.end(); closed = true; @@ -103,7 +108,7 @@ export class ElectronExtensionHostDebugBroadcastChannel extends Extens }); await new Promise(r => server.listen(0, r)); - codeWindow.win.on('close', () => server.close()); + win.on('close', () => server.close()); return { rendererDebugPort: (server.address() as AddressInfo).port }; } diff --git a/src/vs/platform/driver/electron-main/driver.ts b/src/vs/platform/driver/electron-main/driver.ts index 62d7cb313e6..f2f76df80c3 100644 --- a/src/vs/platform/driver/electron-main/driver.ts +++ b/src/vs/platform/driver/electron-main/driver.ts @@ -62,7 +62,7 @@ export class Driver implements IDriver, IWindowDriverRegistry { await this.whenUnfrozen(windowId); const window = this.windowsMainService.getWindowById(windowId); - if (!window) { + if (!window?.win) { throw new Error('Invalid window'); } const webContents = window.win.webContents; @@ -101,7 +101,7 @@ export class Driver implements IDriver, IWindowDriverRegistry { } const window = this.windowsMainService.getWindowById(windowId); - if (!window) { + if (!window?.win) { throw new Error('Invalid window'); } const webContents = window.win.webContents; @@ -207,10 +207,10 @@ export class Driver implements IDriver, IWindowDriverRegistry { export async function serve( windowServer: IPCServer, handle: string, - environmentService: IEnvironmentMainService, + environmentMainService: IEnvironmentMainService, instantiationService: IInstantiationService ): Promise { - const verbose = environmentService.driverVerbose; + const verbose = environmentMainService.driverVerbose; const driver = instantiationService.createInstance(Driver, windowServer, { verbose }); const windowDriverRegistryChannel = new WindowDriverRegistryChannel(driver); diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index f5c79cc4f0d..b9bf96a83e4 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as os from 'os'; +import { homedir, tmpdir } from 'os'; +import product from 'vs/platform/product/common/product'; import { IDebugParams, IExtensionHostDebugParams, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; -import * as paths from 'vs/base/node/paths'; -import * as path from 'vs/base/common/path'; -import * as resources from 'vs/base/common/resources'; +import { getDefaultUserDataPath } from 'vs/base/node/userDataPath'; +import { dirname, join, normalize, resolve } from 'vs/base/common/path'; +import { joinPath } from 'vs/base/common/resources'; import { memoize } from 'vs/base/common/decorators'; -import product from 'vs/platform/product/common/product'; import { toLocalISOString } from 'vs/base/common/date'; import { FileAccess } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; @@ -22,46 +22,46 @@ export class NativeEnvironmentService implements INativeEnvironmentService { get args(): NativeParsedArgs { return this._args; } @memoize - get appRoot(): string { return path.dirname(FileAccess.asFileUri('', require).fsPath); } + get appRoot(): string { return dirname(FileAccess.asFileUri('', require).fsPath); } readonly logsPath: string; @memoize - get userHome(): URI { return URI.file(os.homedir()); } + get userHome(): URI { return URI.file(homedir()); } @memoize get userDataPath(): string { const vscodePortable = process.env['VSCODE_PORTABLE']; if (vscodePortable) { - return path.join(vscodePortable, 'user-data'); + return join(vscodePortable, 'user-data'); } return parseUserDataDir(this._args, process); } @memoize - get appSettingsHome(): URI { return URI.file(path.join(this.userDataPath, 'User')); } + get appSettingsHome(): URI { return URI.file(join(this.userDataPath, 'User')); } @memoize - get tmpDir(): URI { return URI.file(os.tmpdir()); } + get tmpDir(): URI { return URI.file(tmpdir()); } @memoize get userRoamingDataHome(): URI { return this.appSettingsHome; } @memoize - get settingsResource(): URI { return resources.joinPath(this.userRoamingDataHome, 'settings.json'); } + get settingsResource(): URI { return joinPath(this.userRoamingDataHome, 'settings.json'); } @memoize - get userDataSyncHome(): URI { return resources.joinPath(this.userRoamingDataHome, 'sync'); } + get userDataSyncHome(): URI { return joinPath(this.userRoamingDataHome, 'sync'); } @memoize - get userDataSyncLogResource(): URI { return URI.file(path.join(this.logsPath, 'userDataSync.log')); } + get userDataSyncLogResource(): URI { return URI.file(join(this.logsPath, 'userDataSync.log')); } @memoize get sync(): 'on' | 'off' | undefined { return this.args.sync; } @memoize - get machineSettingsResource(): URI { return resources.joinPath(URI.file(path.join(this.userDataPath, 'Machine')), 'settings.json'); } + get machineSettingsResource(): URI { return joinPath(URI.file(join(this.userDataPath, 'Machine')), 'settings.json'); } @memoize get globalStorageHome(): URI { return URI.joinPath(this.appSettingsHome, 'globalStorage'); } @@ -70,32 +70,32 @@ export class NativeEnvironmentService implements INativeEnvironmentService { get workspaceStorageHome(): URI { return URI.joinPath(this.appSettingsHome, 'workspaceStorage'); } @memoize - get keybindingsResource(): URI { return resources.joinPath(this.userRoamingDataHome, 'keybindings.json'); } + get keybindingsResource(): URI { return joinPath(this.userRoamingDataHome, 'keybindings.json'); } @memoize - get keyboardLayoutResource(): URI { return resources.joinPath(this.userRoamingDataHome, 'keyboardLayout.json'); } + get keyboardLayoutResource(): URI { return joinPath(this.userRoamingDataHome, 'keyboardLayout.json'); } @memoize get argvResource(): URI { const vscodePortable = process.env['VSCODE_PORTABLE']; if (vscodePortable) { - return URI.file(path.join(vscodePortable, 'argv.json')); + return URI.file(join(vscodePortable, 'argv.json')); } - return resources.joinPath(this.userHome, product.dataFolderName, 'argv.json'); + return joinPath(this.userHome, product.dataFolderName, 'argv.json'); } @memoize - get snippetsHome(): URI { return resources.joinPath(this.userRoamingDataHome, 'snippets'); } + get snippetsHome(): URI { return joinPath(this.userRoamingDataHome, 'snippets'); } @memoize get isExtensionDevelopment(): boolean { return !!this._args.extensionDevelopmentPath; } @memoize - get untitledWorkspacesHome(): URI { return URI.file(path.join(this.userDataPath, 'Workspaces')); } + get untitledWorkspacesHome(): URI { return URI.file(join(this.userDataPath, 'Workspaces')); } @memoize - get installSourcePath(): string { return path.join(this.userDataPath, 'installSource'); } + get installSourcePath(): string { return join(this.userDataPath, 'installSource'); } @memoize get builtinExtensionsPath(): string { @@ -103,7 +103,7 @@ export class NativeEnvironmentService implements INativeEnvironmentService { if (fromArgs) { return fromArgs; } else { - return path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', 'extensions')); + return normalize(join(FileAccess.asFileUri('', require).fsPath, '..', 'extensions')); } } @@ -112,7 +112,7 @@ export class NativeEnvironmentService implements INativeEnvironmentService { if (fromArgs) { return fromArgs; } else { - return path.join(this.userDataPath, 'CachedExtensionVSIXs'); + return join(this.userDataPath, 'CachedExtensionVSIXs'); } } @@ -131,10 +131,10 @@ export class NativeEnvironmentService implements INativeEnvironmentService { const vscodePortable = process.env['VSCODE_PORTABLE']; if (vscodePortable) { - return path.join(vscodePortable, 'extensions'); + return join(vscodePortable, 'extensions'); } - return resources.joinPath(this.userHome, product.dataFolderName, 'extensions').fsPath; + return joinPath(this.userHome, product.dataFolderName, 'extensions').fsPath; } @memoize @@ -145,7 +145,7 @@ export class NativeEnvironmentService implements INativeEnvironmentService { if (/^[^:/?#]+?:\/\//.test(p)) { return URI.parse(p); } - return URI.file(path.normalize(p)); + return URI.file(normalize(p)); }); } return undefined; @@ -158,7 +158,7 @@ export class NativeEnvironmentService implements INativeEnvironmentService { if (/^[^:/?#]+?:\/\//.test(s)) { return URI.parse(s); } - return URI.file(path.normalize(s)); + return URI.file(normalize(s)); } return undefined; } @@ -188,7 +188,7 @@ export class NativeEnvironmentService implements INativeEnvironmentService { get logLevel(): string | undefined { return this._args.log; } @memoize - get serviceMachineIdResource(): URI { return resources.joinPath(URI.file(this.userDataPath), 'machineid'); } + get serviceMachineIdResource(): URI { return joinPath(URI.file(this.userDataPath), 'machineid'); } get crashReporterId(): string | undefined { return this._args['crash-reporter-id']; } get crashReporterDirectory(): string | undefined { return this._args['crash-reporter-directory']; } @@ -196,13 +196,13 @@ export class NativeEnvironmentService implements INativeEnvironmentService { get driverHandle(): string | undefined { return this._args['driver']; } @memoize - get telemetryLogResource(): URI { return URI.file(path.join(this.logsPath, 'telemetry.log')); } + get telemetryLogResource(): URI { return URI.file(join(this.logsPath, 'telemetry.log')); } get disableTelemetry(): boolean { return !!this._args['disable-telemetry']; } constructor(protected _args: NativeParsedArgs) { if (!_args.logsPath) { const key = toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, ''); - _args.logsPath = path.join(this.userDataPath, 'logs', key); + _args.logsPath = join(this.userDataPath, 'logs', key); } this.logsPath = _args.logsPath; } @@ -231,15 +231,15 @@ export function parsePathArg(arg: string | undefined, process: NodeJS.Process): // Determine if the arg is relative or absolute, if relative use the original CWD // (VSCODE_CWD), not the potentially overridden one (process.cwd()). - const resolved = path.resolve(arg); + const resolved = resolve(arg); - if (path.normalize(arg) === resolved) { + if (normalize(arg) === resolved) { return resolved; } - return path.resolve(process.env['VSCODE_CWD'] || process.cwd(), arg); + return resolve(process.env['VSCODE_CWD'] || process.cwd(), arg); } export function parseUserDataDir(args: NativeParsedArgs, process: NodeJS.Process): string { - return parsePathArg(args['user-data-dir'], process) || path.resolve(paths.getDefaultUserDataPath()); + return parsePathArg(args['user-data-dir'], process) || resolve(getDefaultUserDataPath()); } diff --git a/src/vs/platform/issue/electron-main/issueMainService.ts b/src/vs/platform/issue/electron-main/issueMainService.ts index f6efb42c42d..f79bb21926c 100644 --- a/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/src/vs/platform/issue/electron-main/issueMainService.ts @@ -38,7 +38,7 @@ export class IssueMainService implements ICommonIssueService { constructor( private machineId: string, private userEnv: IProcessEnvironment, - @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, + @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ILaunchMainService private readonly launchMainService: ILaunchMainService, @ILogService private readonly logService: ILogService, @IDiagnosticsService private readonly diagnosticsService: IDiagnosticsService, @@ -271,7 +271,7 @@ export class IssueMainService implements ICommonIssueService { this._processExplorerWindow.setMenuBarVisibility(false); const windowConfiguration = { - appRoot: this.environmentService.appRoot, + appRoot: this.environmentMainService.appRoot, windowId: this._processExplorerWindow.id, userEnv: this.userEnv, machineId: this.machineId, @@ -397,13 +397,13 @@ export class IssueMainService implements ICommonIssueService { } const windowConfiguration = { - appRoot: this.environmentService.appRoot, + appRoot: this.environmentMainService.appRoot, windowId: this._issueWindow.id, machineId: this.machineId, userEnv: this.userEnv, data, features, - disableExtensions: this.environmentService.disableExtensions, + disableExtensions: this.environmentMainService.disableExtensions, os: { type: os.type(), arch: os.arch(), diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 0ec1d10331c..c613f990407 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -21,6 +21,7 @@ import { IMainProcessInfo, IWindowInfo } from 'vs/platform/launch/common/launch' import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper'; import { CancellationToken } from 'vs/base/common/cancellation'; import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { assertIsDefined } from 'vs/base/common/types'; export const ID = 'launchMainService'; export const ILaunchMainService = createDecorator(ID); @@ -293,8 +294,9 @@ export class LaunchMainService implements ILaunchMainService { private codeWindowToInfo(window: ICodeWindow): IWindowInfo { const folderURIs = this.getFolderURIs(window); + const win = assertIsDefined(window.win); - return this.browserWindowToInfo(window.win, folderURIs, window.remoteAuthority); + return this.browserWindowToInfo(win, folderURIs, window.remoteAuthority); } private browserWindowToInfo(window: BrowserWindow, folderURIs: URI[] = [], remoteAuthority?: string): IWindowInfo { diff --git a/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts b/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts index 29a7082b742..82f80545895 100644 --- a/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts +++ b/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts @@ -11,9 +11,11 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; import { handleVetos } from 'vs/platform/lifecycle/common/lifecycle'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Promises, Barrier, timeout } from 'vs/base/common/async'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; +import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { assertIsDefined } from 'vs/base/common/types'; export const ILifecycleMainService = createDecorator('lifecycleMainService'); @@ -24,6 +26,11 @@ export const enum UnloadReason { LOAD = 4 } +export interface IWindowLoadEvent { + window: ICodeWindow; + workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined; +} + export interface IWindowUnloadEvent { window: ICodeWindow; reason: UnloadReason; @@ -72,16 +79,27 @@ export interface ILifecycleMainService { readonly onWillShutdown: Event; /** - * An event that fires before a window closes. This event is fired after any veto has been dealt - * with so that listeners know for sure that the window will close without veto. + * An event that fires when a window is loading. This can either be a window opening for the + * first time or a window reloading or changing to another URL. */ - readonly onBeforeWindowClose: Event; + readonly onWillLoadWindow: Event; /** * An event that fires before a window is about to unload. Listeners can veto this event to prevent * the window from unloading. */ - readonly onBeforeWindowUnload: Event; + readonly onBeforeUnloadWindow: Event; + + /** + * An event that fires before a window closes. This event is fired after any veto has been dealt + * with so that listeners know for sure that the window will close without veto. + */ + readonly onBeforeCloseWindow: Event; + + /** + * Make a `ICodeWindow` known to the lifecycle main service. + */ + registerWindow(window: ICodeWindow): void; /** * Reload a window. All lifecycle event handlers are triggered. @@ -147,11 +165,14 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe private readonly _onWillShutdown = this._register(new Emitter()); readonly onWillShutdown = this._onWillShutdown.event; - private readonly _onBeforeWindowClose = this._register(new Emitter()); - readonly onBeforeWindowClose = this._onBeforeWindowClose.event; + private readonly _onWillLoadWindow = this._register(new Emitter()); + readonly onWillLoadWindow = this._onWillLoadWindow.event; - private readonly _onBeforeWindowUnload = this._register(new Emitter()); - readonly onBeforeWindowUnload = this._onBeforeWindowUnload.event; + private readonly _onBeforeCloseWindow = this._register(new Emitter()); + readonly onBeforeCloseWindow = this._onBeforeCloseWindow.event; + + private readonly _onBeforeUnloadWindow = this._register(new Emitter()); + readonly onBeforeUnloadWindow = this._onBeforeUnloadWindow.event; private _quitRequested = false; get quitRequested(): boolean { return this._quitRequested; } @@ -314,12 +335,17 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe } registerWindow(window: ICodeWindow): void { + const windowListeners = new DisposableStore(); // track window count this.windowCounter++; + // Window Will Load + windowListeners.add(window.onWillLoad(e => this._onWillLoadWindow.fire({ window, workspace: e.workspace }))); + // Window Before Closing: Main -> Renderer - window.win.on('close', e => { + const win = assertIsDefined(window.win); + win.on('close', e => { // The window already acknowledged to be closed const windowId = window.id; @@ -341,9 +367,9 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe this.windowToCloseRequest.add(windowId); - // Fire onBeforeWindowClose before actually closing - this.logService.trace(`Lifecycle#onBeforeWindowClose.fire() - window ID ${windowId}`); - this._onBeforeWindowClose.fire(window); + // Fire onBeforeCloseWindow before actually closing + this.logService.trace(`Lifecycle#onBeforeCloseWindow.fire() - window ID ${windowId}`); + this._onBeforeCloseWindow.fire(window); // No veto, close window now window.close(); @@ -351,12 +377,15 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe }); // Window After Closing - window.win.on('closed', () => { + win.on('closed', () => { this.logService.trace(`Lifecycle#window.on('closed') - window ID ${window.id}`); // update window count this.windowCounter--; + // clear window listeners + windowListeners.dispose(); + // if there are no more code windows opened, fire the onWillShutdown event, unless // we are on macOS where it is perfectly fine to close the last window and // the application continues running (unless quit was actually requested) @@ -452,7 +481,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe private onBeforeUnloadWindowInMain(window: ICodeWindow, reason: UnloadReason): Promise { const vetos: (boolean | Promise)[] = []; - this._onBeforeWindowUnload.fire({ + this._onBeforeUnloadWindow.fire({ reason, window, veto(value) { diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 1d515e5bf6a..752fdb0e4e1 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -852,7 +852,6 @@ function workbenchTreeDataPreamble(treeExpandMode) === 'doubleClick') } as TOptions }; @@ -952,9 +951,6 @@ class WorkbenchTreeInternals { const horizontalScrolling = configurationService.getValue(horizontalScrollingKey); newOptions = { ...newOptions, horizontalScrolling }; } - if (e.affectsConfiguration(openModeSettingKey)) { - newOptions = { ...newOptions, expandOnlyOnDoubleClick: configurationService.getValue(openModeSettingKey) === 'doubleClick' }; - } if (e.affectsConfiguration(treeExpandMode) && options.expandOnlyOnTwistieClick === undefined) { newOptions = { ...newOptions, expandOnlyOnTwistieClick: configurationService.getValue<'singleClick' | 'doubleClick'>(treeExpandMode) === 'doubleClick' }; } @@ -1021,7 +1017,7 @@ configurationRegistry.registerConfiguration({ 'description': localize({ key: 'openModeModifier', comment: ['`singleClick` and `doubleClick` refers to a value the setting can take and should not be localized.'] - }, "Controls how to open items in trees and lists using the mouse (if supported). For parents with children in trees, this setting will control if a single click expands the parent or a double click. Note that some trees and lists might choose to ignore this setting if it is not applicable. ") + }, "Controls how to open items in trees and lists using the mouse (if supported). For parents with children in trees, this setting will control if a single click expands the parent or a double click. Note that some trees and lists might choose to ignore this setting if it is not applicable.") }, [horizontalScrollingKey]: { 'type': 'boolean', @@ -1065,8 +1061,8 @@ configurationRegistry.registerConfiguration({ [treeExpandMode]: { type: 'string', enum: ['singleClick', 'doubleClick'], - default: 'singleClick', - description: localize('expand mode', "Controls how tree folders are expanded when clicking the folder names."), + default: 'doubleClick', + description: localize('expand mode', "Controls how tree folders are expanded when clicking the folder names. Note that some trees and lists might choose to ignore this setting if it is not applicable."), } } }); diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 32f6b799eef..87c5b80744b 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -67,7 +67,7 @@ export class Menubar { @IUpdateService private readonly updateService: IUpdateService, @IConfigurationService private readonly configurationService: IConfigurationService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, + @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService, @IStateService private readonly stateService: IStateService, @@ -168,9 +168,9 @@ export class Menubar { this.lifecycleMainService.onWillShutdown(() => this.willShutdown = true); // // Listen to some events from window service to update menu - this.windowsMainService.onWindowsCountChanged(e => this.onWindowsCountChanged(e)); - this.nativeHostMainService.onDidBlurWindow(() => this.onWindowFocusChange()); - this.nativeHostMainService.onDidFocusWindow(() => this.onWindowFocusChange()); + this.windowsMainService.onDidChangeWindowsCount(e => this.onDidChangeWindowsCount(e)); + this.nativeHostMainService.onDidBlurWindow(() => this.onDidChangeWindowFocus()); + this.nativeHostMainService.onDidFocusWindow(() => this.onDidChangeWindowFocus()); } private get currentEnableMenuBarMnemonics(): boolean { @@ -225,7 +225,7 @@ export class Menubar { } } - private onWindowsCountChanged(e: IWindowsCountChangedEvent): void { + private onDidChangeWindowsCount(e: IWindowsCountChangedEvent): void { if (!isMacintosh) { return; } @@ -237,7 +237,7 @@ export class Menubar { } } - private onWindowFocusChange(): void { + private onDidChangeWindowFocus(): void { if (!isMacintosh) { return; } @@ -499,7 +499,7 @@ export class Menubar { const openInNewWindow = this.isOptionClick(event); const success = this.windowsMainService.open({ context: OpenContext.MENU, - cli: this.environmentService.args, + cli: this.environmentMainService.args, urisToOpen: [openable], forceNewWindow: openInNewWindow, gotoLineMode: false @@ -716,7 +716,7 @@ export class Menubar { if (activeWindow) { this.logService.trace('menubar#runActionInRenderer', invocation); - if (isMacintosh && !this.environmentService.isBuilt && !activeWindow.isReady) { + if (isMacintosh && !this.environmentMainService.isBuilt && !activeWindow.isReady) { if ((invocation.type === 'commandId' && invocation.commandId === 'workbench.action.toggleDevTools') || (invocation.type !== 'commandId' && invocation.userSettingsLabel === 'alt+cmd+i')) { // prevent this action from running twice on macOS (https://github.com/microsoft/vscode/issues/62719) // we already register a keybinding in bootstrap-window.js for opening developer tools in case something diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 406fd62d878..ba9d54e54fd 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -47,7 +47,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @IDialogMainService private readonly dialogMainService: IDialogMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, - @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, + @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService ) { @@ -76,14 +76,14 @@ export class NativeHostMainService extends Disposable implements INativeHostMain //#region Events - readonly onDidOpenWindow = Event.map(this.windowsMainService.onWindowOpened, window => window.id); + readonly onDidOpenWindow = Event.map(this.windowsMainService.onDidOpenWindow, window => window.id); readonly onDidMaximizeWindow = Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-maximize', (event, window: BrowserWindow) => window.id), windowId => !!this.windowsMainService.getWindowById(windowId)); readonly onDidUnmaximizeWindow = Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-unmaximize', (event, window: BrowserWindow) => window.id), windowId => !!this.windowsMainService.getWindowById(windowId)); readonly onDidBlurWindow = Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-blur', (event, window: BrowserWindow) => window.id), windowId => !!this.windowsMainService.getWindowById(windowId)); readonly onDidFocusWindow = Event.any( - Event.map(Event.filter(Event.map(this.windowsMainService.onWindowsCountChanged, () => this.windowsMainService.getLastActiveWindow()), window => !!window), window => window!.id), + Event.map(Event.filter(Event.map(this.windowsMainService.onDidChangeWindowsCount, () => this.windowsMainService.getLastActiveWindow()), window => !!window), window => window!.id), Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-focus', (event, window: BrowserWindow) => window.id), windowId => !!this.windowsMainService.getWindowById(windowId)) ); @@ -105,7 +105,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain return windows.map(window => ({ id: window.id, workspace: window.openedWorkspace, - title: window.win.getTitle(), + title: window.win?.getTitle() ?? '', filename: window.getRepresentedFilename(), dirty: window.isDocumentEdited() })); @@ -140,7 +140,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain context: OpenContext.API, contextWindowId: windowId, urisToOpen: toOpen, - cli: this.environmentService.args, + cli: this.environmentMainService.args, forceNewWindow: options.forceNewWindow, forceReuseWindow: options.forceReuseWindow, preferNewWindow: options.preferNewWindow, @@ -176,7 +176,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async isMaximized(windowId: number | undefined): Promise { const window = this.windowById(windowId); - if (window) { + if (window?.win) { return window.win.isMaximized(); } @@ -185,21 +185,21 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async maximizeWindow(windowId: number | undefined): Promise { const window = this.windowById(windowId); - if (window) { + if (window?.win) { window.win.maximize(); } } async unmaximizeWindow(windowId: number | undefined): Promise { const window = this.windowById(windowId); - if (window) { + if (window?.win) { window.win.unmaximize(); } } async minimizeWindow(windowId: number | undefined): Promise { const window = this.windowById(windowId); - if (window) { + if (window?.win) { window.win.minimize(); } } @@ -217,7 +217,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async setMinimumSize(windowId: number | undefined, width: number | undefined, height: number | undefined): Promise { const window = this.windowById(windowId); - if (window) { + if (window?.win) { const [windowWidth, windowHeight] = window.win.getSize(); const [minWindowWidth, minWindowHeight] = window.win.getMinimumSize(); const [newMinWindowWidth, newMinWindowHeight] = [width ?? minWindowWidth, height ?? minWindowHeight]; @@ -250,7 +250,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain private toBrowserWindow(windowId: number | undefined): BrowserWindow | undefined { const window = this.windowById(windowId); - if (window) { + if (window?.win) { return window.win; } @@ -293,7 +293,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain this.windowsMainService.open({ context: OpenContext.DIALOG, contextWindowId: windowId, - cli: this.environmentService.args, + cli: this.environmentMainService.args, urisToOpen: openable, forceNewWindow: options.forceNewWindow }); @@ -386,7 +386,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain const promptOptions = { name: product.nameLong.replace('-', ''), - icns: (isMacintosh && this.environmentService.isBuilt) ? join(dirname(this.environmentService.appRoot), `${product.nameShort}.icns`) : undefined + icns: (isMacintosh && this.environmentMainService.isBuilt) ? join(dirname(this.environmentMainService.appRoot), `${product.nameShort}.icns`) : undefined }; sudoPrompt.exec(sudoCommand.join(' '), promptOptions, (error: string, stdout: string, stderr: string) => { @@ -412,28 +412,28 @@ export class NativeHostMainService extends Disposable implements INativeHostMain // Windows if (isWindows) { - if (this.environmentService.isBuilt) { + if (this.environmentMainService.isBuilt) { return join(dirname(process.execPath), 'bin', `${product.applicationName}.cmd`); } - return join(this.environmentService.appRoot, 'scripts', 'code-cli.bat'); + return join(this.environmentMainService.appRoot, 'scripts', 'code-cli.bat'); } // Linux if (isLinux) { - if (this.environmentService.isBuilt) { + if (this.environmentMainService.isBuilt) { return join(dirname(process.execPath), 'bin', `${product.applicationName}`); } - return join(this.environmentService.appRoot, 'scripts', 'code-cli.sh'); + return join(this.environmentMainService.appRoot, 'scripts', 'code-cli.sh'); } // macOS - if (this.environmentService.isBuilt) { - return join(this.environmentService.appRoot, 'bin', 'code'); + if (this.environmentMainService.isBuilt) { + return join(this.environmentMainService.appRoot, 'bin', 'code'); } - return join(this.environmentService.appRoot, 'scripts', 'code-cli.sh'); + return join(this.environmentMainService.appRoot, 'scripts', 'code-cli.sh'); } async getOSStatistics(): Promise { @@ -505,7 +505,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain //#region macOS Touchbar async newWindowTab(): Promise { - this.windowsMainService.open({ context: OpenContext.API, cli: this.environmentService.args, forceNewTabbedWindow: true, forceEmpty: true }); + this.windowsMainService.open({ context: OpenContext.API, cli: this.environmentMainService.args, forceNewTabbedWindow: true, forceEmpty: true }); } async showPreviousWindowTab(): Promise { @@ -563,7 +563,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async closeWindowById(currentWindowId: number | undefined, targetWindowId?: number | undefined): Promise { const window = this.windowById(targetWindowId); - if (window) { + if (window?.win) { return window.win.close(); } } @@ -573,7 +573,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain // If the user selected to exit from an extension development host window, do not quit, but just // close the window unless this is the last window that is opened. const window = this.windowsMainService.getLastActiveWindow(); - if (window?.isExtensionDevelopmentHost && this.windowsMainService.getWindowCount() > 1) { + if (window?.isExtensionDevelopmentHost && this.windowsMainService.getWindowCount() > 1 && window.win) { window.win.close(); } @@ -609,14 +609,14 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async openDevTools(windowId: number | undefined, options?: OpenDevToolsOptions): Promise { const window = this.windowById(windowId); - if (window) { + if (window?.win) { window.win.webContents.openDevTools(options); } } async toggleDevTools(windowId: number | undefined): Promise { const window = this.windowById(windowId); - if (window) { + if (window?.win) { const contents = window.win.webContents; contents.toggleDevTools(); } @@ -624,7 +624,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async sendInputEvent(windowId: number | undefined, event: MouseInputEvent): Promise { const window = this.windowById(windowId); - if (window && (event.type === 'mouseDown' || event.type === 'mouseUp')) { + if (window?.win && (event.type === 'mouseDown' || event.type === 'mouseUp')) { window.win.webContents.sendInputEvent(event); } } diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 71bb6ae4cd6..2730ee7e8c6 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -28,7 +28,7 @@ if (isWeb || typeof require === 'undefined' || typeof require.__$__nodeRequire ! urlProtocol: 'code-oss', reportIssueUrl: 'https://github.com/microsoft/vscode/issues/new', licenseName: 'MIT', - licenseUrl: 'https://github.com/microsoft/vscode/blob/master/LICENSE.txt', + licenseUrl: 'https://github.com/microsoft/vscode/blob/main/LICENSE.txt', extensionAllowedProposedApi: [ 'ms-vscode.vscode-js-profile-flame', 'ms-vscode.vscode-js-profile-table', diff --git a/src/vs/platform/product/common/productService.ts b/src/vs/platform/product/common/productService.ts index 92b8c1a6d52..3f6ace923a6 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts @@ -45,7 +45,7 @@ export interface IProductConfiguration { readonly applicationName: string; readonly urlProtocol: string; - readonly dataFolderName: string; + readonly dataFolderName: string; // location for extensions (e.g. ~/.vscode-insiders) readonly builtInExtensions?: IBuiltInExtension[]; diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 86f5ddaf348..6c54bf00996 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -7,7 +7,7 @@ import { IQuickInputService, IQuickPickItem, IPickOptions, IInputOptions, IQuick import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; -import { inputBackground, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorForeground, inputValidationErrorBorder, badgeBackground, badgeForeground, contrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, progressBarBackground, widgetShadow, listFocusForeground, listFocusBackground, activeContrastBorder, pickerGroupBorder, pickerGroupForeground, quickInputForeground, quickInputBackground, quickInputTitleBackground } from 'vs/platform/theme/common/colorRegistry'; +import { inputBackground, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorForeground, inputValidationErrorBorder, badgeBackground, badgeForeground, contrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, progressBarBackground, widgetShadow, listFocusForeground, activeContrastBorder, pickerGroupBorder, pickerGroupForeground, quickInputForeground, quickInputBackground, quickInputTitleBackground, quickInputListFocusBackground } from 'vs/platform/theme/common/colorRegistry'; import { CancellationToken } from 'vs/base/common/cancellation'; import { computeStyles } from 'vs/platform/theme/common/styler'; import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -213,7 +213,7 @@ export class QuickInputService extends Themable implements IQuickInputService { listBackground: quickInputBackground, // Look like focused when inactive. listInactiveFocusForeground: listFocusForeground, - listInactiveFocusBackground: listFocusBackground, + listInactiveFocusBackground: quickInputListFocusBackground, listFocusOutline: activeContrastBorder, listInactiveFocusOutline: activeContrastBorder, pickerGroupBorder, diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index c0aad5ffa10..966a8830bf6 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -26,7 +26,7 @@ export class SharedProcess extends Disposable implements ISharedProcess { constructor( private readonly machineId: string, private userEnv: NodeJS.ProcessEnv, - @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, + @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @ILogService private readonly logService: ILogService, @IThemeMainService private readonly themeMainService: IThemeMainService @@ -164,11 +164,11 @@ export class SharedProcess extends Disposable implements ISharedProcess { const config: ISharedProcessConfiguration = { machineId: this.machineId, windowId: this.window.id, - appRoot: this.environmentService.appRoot, - nodeCachedDataDir: this.environmentService.nodeCachedDataDir, - backupWorkspacesPath: this.environmentService.backupWorkspacesPath, + appRoot: this.environmentMainService.appRoot, + nodeCachedDataDir: this.environmentMainService.nodeCachedDataDir, + backupWorkspacesPath: this.environmentMainService.backupWorkspacesPath, userEnv: this.userEnv, - args: this.environmentService.args, + args: this.environmentMainService.args, logLevel: this.logService.getLevel() }; diff --git a/src/vs/platform/storage/browser/storageService.ts b/src/vs/platform/storage/browser/storageService.ts index d7c9c986d84..aa2eb5f6c7d 100644 --- a/src/vs/platform/storage/browser/storageService.ts +++ b/src/vs/platform/storage/browser/storageService.ts @@ -5,7 +5,7 @@ import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; -import { StorageScope, logStorage, IS_NEW_KEY, AbstractStorageService } from 'vs/platform/storage/common/storage'; +import { StorageScope, IS_NEW_KEY, AbstractStorageService } from 'vs/platform/storage/common/storage'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; import { IFileService, FileChangeType } from 'vs/platform/files/common/files'; @@ -14,7 +14,6 @@ import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; import { runWhenIdle, RunOnceScheduler, Promises } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; -import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; export class BrowserStorageService extends AbstractStorageService { @@ -100,58 +99,18 @@ export class BrowserStorageService extends AbstractStorageService { this.periodicFlushScheduler.schedule(); } - get(key: string, scope: StorageScope, fallbackValue: string): string; - get(key: string, scope: StorageScope): string | undefined; - get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined { - return this.getStorage(scope).get(key, fallbackValue); + protected getStorage(scope: StorageScope): IStorage | undefined { + return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage; } - getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean; - getBoolean(key: string, scope: StorageScope): boolean | undefined; - getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined { - return this.getStorage(scope).getBoolean(key, fallbackValue); - } - - getNumber(key: string, scope: StorageScope, fallbackValue: number): number; - getNumber(key: string, scope: StorageScope): number | undefined; - getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined { - return this.getStorage(scope).getNumber(key, fallbackValue); - } - - protected doStore(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): void { - this.getStorage(scope).set(key, value); - } - - protected doRemove(key: string, scope: StorageScope): void { - this.getStorage(scope).delete(key); - } - - private getStorage(scope: StorageScope): IStorage { - return assertIsDefined(scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage); - } - - async logStorage(): Promise { - const [globalStorage, workspaceStorage, globalStorageFile, workspaceStorageFile] = assertAllDefined(this.globalStorage, this.workspaceStorage, this.globalStorageFile, this.workspaceStorageFile); - - const result = await Promise.all([ - globalStorage.items, - workspaceStorage.items - ]); - - return logStorage(result[0], result[1], globalStorageFile.toString(), workspaceStorageFile.toString()); + protected getLogDetails(scope: StorageScope): string | undefined { + return scope === StorageScope.GLOBAL ? this.globalStorageFile?.toString() : this.workspaceStorageFile?.toString(); } async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise { throw new Error('Migrating storage is currently unsupported in Web'); } - protected async doFlush(): Promise { - await Promises.settled([ - this.getStorage(StorageScope.GLOBAL).whenFlushed(), - this.getStorage(StorageScope.WORKSPACE).whenFlushed() - ]); - } - private doFlushWhenIdle(): void { // Dispose any previous idle runner diff --git a/src/vs/platform/storage/common/storage.ts b/src/vs/platform/storage/common/storage.ts index 3d176f5b6d8..aad6f035908 100644 --- a/src/vs/platform/storage/common/storage.ts +++ b/src/vs/platform/storage/common/storage.ts @@ -8,6 +8,8 @@ import { Event, Emitter, PauseableEmitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; +import { InMemoryStorageDatabase, IStorage, Storage } from 'vs/base/parts/storage/common/storage'; +import { Promises } from 'vs/base/common/async'; export const IS_NEW_KEY = '__$__isNewStorageMarker'; const TARGET_KEY = '__$__targetStorageMarker'; @@ -257,6 +259,24 @@ export abstract class AbstractStorageService extends Disposable implements IStor this._onWillSaveState.fire({ reason }); } + get(key: string, scope: StorageScope, fallbackValue: string): string; + get(key: string, scope: StorageScope): string | undefined; + get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined { + return this.getStorage(scope)?.get(key, fallbackValue); + } + + getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean; + getBoolean(key: string, scope: StorageScope): boolean | undefined; + getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined { + return this.getStorage(scope)?.getBoolean(key, fallbackValue); + } + + getNumber(key: string, scope: StorageScope, fallbackValue: number): number; + getNumber(key: string, scope: StorageScope): number | undefined; + getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined { + return this.getStorage(scope)?.getNumber(key, fallbackValue); + } + store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope, target: StorageTarget): void { // We remove the key for undefined/null values @@ -272,7 +292,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor this.updateKeyTarget(key, scope, target); // Store actual value - this.doStore(key, value, scope); + this.getStorage(scope)?.set(key, value); }); } @@ -285,7 +305,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor this.updateKeyTarget(key, scope, undefined); // Remove actual key - this.doRemove(key, scope); + this.getStorage(scope)?.delete(key); }); } @@ -326,7 +346,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor if (typeof target === 'number') { if (keyTargets[key] !== target) { keyTargets[key] = target; - this.doStore(TARGET_KEY, JSON.stringify(keyTargets), scope); + this.getStorage(scope)?.set(TARGET_KEY, JSON.stringify(keyTargets)); } } @@ -334,7 +354,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor else { if (typeof keyTargets[key] === 'number') { delete keyTargets[key]; - this.doStore(TARGET_KEY, JSON.stringify(keyTargets), scope); + this.getStorage(scope)?.set(TARGET_KEY, JSON.stringify(keyTargets)); } } } @@ -378,118 +398,62 @@ export abstract class AbstractStorageService extends Disposable implements IStor return this.getBoolean(IS_NEW_KEY, scope) === true; } - flush(): Promise { + async flush(): Promise { // Signal event to collect changes this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE }); // Await flush - return this.doFlush(); + await Promises.settled([ + this.getStorage(StorageScope.GLOBAL)?.whenFlushed() ?? Promise.resolve(), + this.getStorage(StorageScope.WORKSPACE)?.whenFlushed() ?? Promise.resolve() + ]); + } + + async logStorage(): Promise { + const globalItems = this.getStorage(StorageScope.GLOBAL)?.items ?? new Map(); + const workspaceItems = this.getStorage(StorageScope.WORKSPACE)?.items ?? new Map(); + + return logStorage( + globalItems, + workspaceItems, + this.getLogDetails(StorageScope.GLOBAL) ?? '', + this.getLogDetails(StorageScope.WORKSPACE) ?? '' + ); } // --- abstract - abstract get(key: string, scope: StorageScope, fallbackValue: string): string; - abstract get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined; + protected abstract getStorage(scope: StorageScope): IStorage | undefined; - abstract getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean; - abstract getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined; - - abstract getNumber(key: string, scope: StorageScope, fallbackValue: number): number; - abstract getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined; - - protected abstract doStore(key: string, value: string | boolean | number, scope: StorageScope): void; - - protected abstract doRemove(key: string, scope: StorageScope): void; - - protected abstract doFlush(): Promise; + protected abstract getLogDetails(scope: StorageScope): string | undefined; abstract migrate(toWorkspace: IWorkspaceInitializationPayload): Promise; - - abstract logStorage(): void; } export class InMemoryStorageService extends AbstractStorageService { - private readonly globalCache = new Map(); - private readonly workspaceCache = new Map(); + private globalStorage = new Storage(new InMemoryStorageDatabase()); + private workspaceStorage = new Storage(new InMemoryStorageDatabase()); - private getCache(scope: StorageScope): Map { - return scope === StorageScope.GLOBAL ? this.globalCache : this.workspaceCache; + constructor() { + super(); + + this._register(this.workspaceStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key))); + this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key))); } - get(key: string, scope: StorageScope, fallbackValue: string): string; - get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined { - const value = this.getCache(scope).get(key); - - if (isUndefinedOrNull(value)) { - return fallbackValue; - } - - return value; + protected getStorage(scope: StorageScope): IStorage { + return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage; } - getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean; - getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined { - const value = this.getCache(scope).get(key); - - if (isUndefinedOrNull(value)) { - return fallbackValue; - } - - return value === 'true'; - } - - getNumber(key: string, scope: StorageScope, fallbackValue: number): number; - getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined { - const value = this.getCache(scope).get(key); - - if (isUndefinedOrNull(value)) { - return fallbackValue; - } - - return parseInt(value, 10); - } - - protected doStore(key: string, value: string | boolean | number, scope: StorageScope): void { - - // Otherwise, convert to String and store - const valueStr = String(value); - - // Return early if value already set - const currentValue = this.getCache(scope).get(key); - if (currentValue === valueStr) { - return; - } - - // Update in cache - this.getCache(scope).set(key, valueStr); - - // Events - this.emitDidChangeValue(scope, key); - } - - protected doRemove(key: string, scope: StorageScope): void { - const wasDeleted = this.getCache(scope).delete(key); - if (!wasDeleted) { - return; // Return early if value already deleted - } - - // Events - this.emitDidChangeValue(scope, key); - } - - logStorage(): void { - logStorage(this.globalCache, this.workspaceCache, 'inMemory', 'inMemory'); + protected getLogDetails(scope: StorageScope): string | undefined { + return scope === StorageScope.GLOBAL ? 'inMemory (global)' : 'inMemory (workspace)'; } async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise { // not supported } - - async doFlush(): Promise { } - - async close(): Promise { } } export async function logStorage(global: Map, workspace: Map, globalPath: string, workspacePath: string): Promise { diff --git a/src/vs/platform/storage/common/storageIpc.ts b/src/vs/platform/storage/common/storageIpc.ts new file mode 100644 index 00000000000..fe88c0815c7 --- /dev/null +++ b/src/vs/platform/storage/common/storageIpc.ts @@ -0,0 +1,116 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage'; +import { IEmptyWorkspaceIdentifier, ISerializedSingleFolderWorkspaceIdentifier, ISerializedWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; + +export type Key = string; +export type Value = string; +export type Item = [Key, Value]; + +export interface IBaseSerializableStorageRequest { + readonly workspace: ISerializedWorkspaceIdentifier | ISerializedSingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined +} + +export interface ISerializableUpdateRequest extends IBaseSerializableStorageRequest { + insert?: Item[]; + delete?: Key[]; +} + +export interface ISerializableItemsChangeEvent { + readonly changed?: Item[]; + readonly deleted?: Key[]; +} + +abstract class BaseStorageDatabaseClient extends Disposable implements IStorageDatabase { + + abstract onDidChangeItemsExternal: Event; + + constructor(protected channel: IChannel, private workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined) { + super(); + } + + async getItems(): Promise> { + const serializableRequest: IBaseSerializableStorageRequest = { workspace: this.workspace }; + const items: Item[] = await this.channel.call('getItems', serializableRequest); + + return new Map(items); + } + + updateItems(request: IUpdateRequest): Promise { + const serializableRequest: ISerializableUpdateRequest = { workspace: this.workspace }; + + if (request.insert) { + serializableRequest.insert = Array.from(request.insert.entries()); + } + + if (request.delete) { + serializableRequest.delete = Array.from(request.delete.values()); + } + + return this.channel.call('updateItems', serializableRequest); + } + + async close(): Promise { + + // The database connection is not owned by us, but rather on the + // main side, as such we do not forward the close() request but + // let main side handle this properly via lifecycle methods. + // + // However, we cleanup our listeners because we are no longer + // interested in change events from the global database + this.dispose(); + } +} + +class GlobalStorageDatabaseClient extends BaseStorageDatabaseClient implements IStorageDatabase { + + private readonly _onDidChangeItemsExternal = this._register(new Emitter()); + readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event; + + constructor(channel: IChannel) { + super(channel, undefined); + + this.registerListeners(); + } + + private registerListeners(): void { + this._register(this.channel.listen('onDidChangeGlobalStorage')((e: ISerializableItemsChangeEvent) => this.onDidChangeGlobalStorage(e))); + } + + private onDidChangeGlobalStorage(e: ISerializableItemsChangeEvent): void { + if (Array.isArray(e.changed) || Array.isArray(e.deleted)) { + this._onDidChangeItemsExternal.fire({ + changed: e.changed ? new Map(e.changed) : undefined, + deleted: e.deleted ? new Set(e.deleted) : undefined + }); + } + } +} + +class WorkspaceStorageDatabaseClient extends BaseStorageDatabaseClient implements IStorageDatabase { + + readonly onDidChangeItemsExternal = Event.None; // unsupported for workspace storage because we only ever write from one window + + constructor(channel: IChannel, workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier) { + super(channel, workspace); + } +} + +export class StorageDatabaseChannelClient extends Disposable { + + readonly globalStorage = new GlobalStorageDatabaseClient(this.channel); + readonly workspaceStorage = this.workspace ? new WorkspaceStorageDatabaseClient(this.channel, this.workspace) : undefined; + + constructor( + private channel: IChannel, + private workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined + ) { + super(); + } +} diff --git a/src/vs/platform/storage/electron-main/storageIpc.ts b/src/vs/platform/storage/electron-main/storageIpc.ts new file mode 100644 index 00000000000..2fb542e34cf --- /dev/null +++ b/src/vs/platform/storage/electron-main/storageIpc.ts @@ -0,0 +1,123 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ISerializableItemsChangeEvent, ISerializableUpdateRequest, IBaseSerializableStorageRequest, Key, Value } from 'vs/platform/storage/common/storageIpc'; +import { IStorageChangeEvent, IStorageMain } from 'vs/platform/storage/electron-main/storageMain'; +import { IStorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; +import { IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, reviveIdentifier } from 'vs/platform/workspaces/common/workspaces'; + +export class StorageDatabaseChannel extends Disposable implements IServerChannel { + + private static readonly STORAGE_CHANGE_DEBOUNCE_TIME = 100; + + private readonly _onDidChangeGlobalStorage = this._register(new Emitter()); + private readonly onDidChangeGlobalStorage = this._onDidChangeGlobalStorage.event; + + constructor( + private logService: ILogService, + private storageMainService: IStorageMainService + ) { + super(); + + this.registerGlobalStorageListeners(); + } + + //#region Global Storage Change Events + + private registerGlobalStorageListeners(): void { + + // Listen for changes in global storage to send to listeners + // that are listening. Use a debouncer to reduce IPC traffic. + this._register(Event.debounce(this.storageMainService.globalStorage.onDidChangeStorage, (prev: IStorageChangeEvent[] | undefined, cur: IStorageChangeEvent) => { + if (!prev) { + prev = [cur]; + } else { + prev.push(cur); + } + + return prev; + }, StorageDatabaseChannel.STORAGE_CHANGE_DEBOUNCE_TIME)(events => { + if (events.length) { + this._onDidChangeGlobalStorage.fire(this.serializeGlobalStorageEvents(events)); + } + })); + } + + private serializeGlobalStorageEvents(events: IStorageChangeEvent[]): ISerializableItemsChangeEvent { + const changed = new Map(); + const deleted = new Set(); + events.forEach(event => { + const existing = this.storageMainService.globalStorage.get(event.key); + if (typeof existing === 'string') { + changed.set(event.key, existing); + } else { + deleted.add(event.key); + } + }); + + return { + changed: Array.from(changed.entries()), + deleted: Array.from(deleted.values()) + }; + } + + listen(_: unknown, event: string): Event { + switch (event) { + case 'onDidChangeGlobalStorage': return this.onDidChangeGlobalStorage; + } + + throw new Error(`Event not found: ${event}`); + } + + //#endregion + + async call(_: unknown, command: string, arg: IBaseSerializableStorageRequest): Promise { + const workspace = reviveIdentifier(arg.workspace); + + // Get storage to be ready + const storage = await this.withStorageInitialized(workspace); + + // handle call + switch (command) { + case 'getItems': { + return Array.from(storage.items.entries()); + } + + case 'updateItems': { + const items: ISerializableUpdateRequest = arg; + if (items.insert) { + for (const [key, value] of items.insert) { + storage.store(key, value); + } + } + + if (items.delete) { + items.delete.forEach(key => storage.remove(key)); + } + + break; + } + + default: + throw new Error(`Call not found: ${command}`); + } + } + + private async withStorageInitialized(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined): Promise { + const storage = workspace ? this.storageMainService.workspaceStorage(workspace) : this.storageMainService.globalStorage; + + try { + await storage.initialize(); + } catch (error) { + this.logService.error(`StorageIPC#init: Unable to init ${workspace ? 'workspace' : 'global'} storage due to ${error}`); + } + + return storage; + } +} diff --git a/src/vs/platform/storage/electron-main/storageMain.ts b/src/vs/platform/storage/electron-main/storageMain.ts new file mode 100644 index 00000000000..9d1f5c1b6ce --- /dev/null +++ b/src/vs/platform/storage/electron-main/storageMain.ts @@ -0,0 +1,331 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { promises } from 'fs'; +import { exists, writeFile } from 'vs/base/node/pfs'; +import { Event, Emitter } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { ILogService, LogLevel } from 'vs/platform/log/common/log'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage'; +import { Storage, InMemoryStorageDatabase, StorageHint, IStorage } from 'vs/base/parts/storage/common/storage'; +import { join } from 'vs/base/common/path'; +import { IS_NEW_KEY } from 'vs/platform/storage/common/storage'; +import { currentSessionDateStorageKey, firstSessionDateStorageKey, instanceStorageKey, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; +import { generateUuid } from 'vs/base/common/uuid'; +import { IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; + +export interface IStorageMainOptions { + + /** + * If enabled, storage will not persist to disk + * but into memory. + */ + useInMemoryStorage?: boolean; +} + +/** + * Provides access to global and workspace storage from the + * electron-main side that is the owner of all storage connections. + */ +export interface IStorageMain extends IDisposable { + + /** + * Emitted whenever data is updated or deleted. + */ + readonly onDidChangeStorage: Event; + + /** + * Emitted when the storage is closed. + */ + readonly onDidCloseStorage: Event; + + /** + * Access to all cached items of this storage service. + */ + readonly items: Map; + + /** + * Required call to ensure the service can be used. + */ + initialize(): Promise; + + /** + * Retrieve an element stored with the given key from storage. Use + * the provided defaultValue if the element is null or undefined. + */ + get(key: string, fallbackValue: string): string; + get(key: string, fallbackValue?: string): string | undefined; + + /** + * Retrieve an element stored with the given key from storage. Use + * the provided defaultValue if the element is null or undefined. The element + * will be converted to a boolean. + */ + getBoolean(key: string, fallbackValue: boolean): boolean; + getBoolean(key: string, fallbackValue?: boolean): boolean | undefined; + + /** + * Retrieve an element stored with the given key from storage. Use + * the provided defaultValue if the element is null or undefined. The element + * will be converted to a number using parseInt with a base of 10. + */ + getNumber(key: string, fallbackValue: number): number; + getNumber(key: string, fallbackValue?: number): number | undefined; + + /** + * Store a string value under the given key to storage. The value will + * be converted to a string. + */ + store(key: string, value: string | boolean | number | undefined | null): void; + + /** + * Delete an element stored under the provided key from storage. + */ + remove(key: string): void; + + /** + * Close the storage connection. + */ + close(): Promise; +} + +export interface IStorageChangeEvent { + key: string; +} + +abstract class BaseStorageMain extends Disposable implements IStorageMain { + + protected readonly _onDidChangeStorage = this._register(new Emitter()); + readonly onDidChangeStorage = this._onDidChangeStorage.event; + + private readonly _onDidCloseStorage = this._register(new Emitter()); + readonly onDidCloseStorage = this._onDidCloseStorage.event; + + private storage: IStorage = new Storage(new InMemoryStorageDatabase()); // storage is in-memory until initialized + + private initializePromise: Promise | undefined = undefined; + + constructor( + protected readonly logService: ILogService + ) { + super(); + } + + initialize(): Promise { + if (!this.initializePromise) { + this.initializePromise = (async () => { + try { + const storage = await this.doInitialize(); + + // Replace our in-memory storage with the initialized + // one once that is finished and use it from then on + this.storage.dispose(); + this.storage = storage; + + // Ensure we track wether storage is new or not + const isNewStorage = storage.getBoolean(IS_NEW_KEY); + if (isNewStorage === undefined) { + storage.set(IS_NEW_KEY, true); + } else if (isNewStorage) { + storage.set(IS_NEW_KEY, false); + } + } catch (error) { + this.logService.error(`StorageMain#initialize(): Unable to init storage due to ${error}`); + } + })(); + } + + return this.initializePromise; + } + + protected createLogginOptions(): ISQLiteStorageDatabaseLoggingOptions { + return { + logTrace: (this.logService.getLevel() === LogLevel.Trace) ? msg => this.logService.trace(msg) : undefined, + logError: error => this.logService.error(error) + }; + } + + protected abstract doInitialize(): Promise; + + get items(): Map { return this.storage.items; } + + get(key: string, fallbackValue: string): string; + get(key: string, fallbackValue?: string): string | undefined; + get(key: string, fallbackValue?: string): string | undefined { + return this.storage.get(key, fallbackValue); + } + + getBoolean(key: string, fallbackValue: boolean): boolean; + getBoolean(key: string, fallbackValue?: boolean): boolean | undefined; + getBoolean(key: string, fallbackValue?: boolean): boolean | undefined { + return this.storage.getBoolean(key, fallbackValue); + } + + getNumber(key: string, fallbackValue: number): number; + getNumber(key: string, fallbackValue?: number): number | undefined; + getNumber(key: string, fallbackValue?: number): number | undefined { + return this.storage.getNumber(key, fallbackValue); + } + + store(key: string, value: string | boolean | number | undefined | null): Promise { + return this.storage.set(key, value); + } + + remove(key: string): Promise { + return this.storage.delete(key); + } + + async close(): Promise { + + // Propagate to storage lib + await this.storage.close(); + + // Signal as event + this._onDidCloseStorage.fire(); + } +} + +export class GlobalStorageMain extends BaseStorageMain implements IStorageMain { + + private static readonly STORAGE_NAME = 'state.vscdb'; + + constructor( + private readonly options: IStorageMainOptions, + logService: ILogService, + private readonly environmentService: IEnvironmentService + ) { + super(logService); + } + + protected async doInitialize(): Promise { + let storagePath: string; + if (this.options.useInMemoryStorage) { + storagePath = SQLiteStorageDatabase.IN_MEMORY_PATH; + } else { + storagePath = join(this.environmentService.globalStorageHome.fsPath, GlobalStorageMain.STORAGE_NAME); + } + + // Create Storage + const storage = new Storage(new SQLiteStorageDatabase(storagePath, { + logging: this.createLogginOptions() + })); + + // Re-emit storage changes via event + this._register(storage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key }))); + + // Forward init to SQLite DB + await storage.init(); + + // Apply global telemetry values as part of the initialization + this.updateTelemetryState(storage); + + return storage; + } + + private updateTelemetryState(storage: Storage): void { + + // Instance UUID (once) + const instanceId = storage.get(instanceStorageKey, undefined); + if (instanceId === undefined) { + storage.set(instanceStorageKey, generateUuid()); + } + + // First session date (once) + const firstSessionDate = storage.get(firstSessionDateStorageKey, undefined); + if (firstSessionDate === undefined) { + storage.set(firstSessionDateStorageKey, new Date().toUTCString()); + } + + // Last / current session (always) + // previous session date was the "current" one at that time + // current session date is "now" + const lastSessionDate = storage.get(currentSessionDateStorageKey, undefined); + const currentSessionDate = new Date().toUTCString(); + storage.set(lastSessionDateStorageKey, typeof lastSessionDate === 'undefined' ? null : lastSessionDate); + storage.set(currentSessionDateStorageKey, currentSessionDate); + } +} + +export class WorkspaceStorageMain extends BaseStorageMain implements IStorageMain { + + private static readonly WORKSPACE_STORAGE_NAME = 'state.vscdb'; + private static readonly WORKSPACE_META_NAME = 'workspace.json'; + + constructor( + private workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier, + private readonly options: IStorageMainOptions, + logService: ILogService, + private readonly environmentService: IEnvironmentService + ) { + super(logService); + } + + protected async doInitialize(): Promise { + + // Prepare workspace storage folder for DB + const { storageFilePath, wasCreated } = await this.prepareWorkspaceStorageFolder(); + + // Create Storage + const storage = new Storage(new SQLiteStorageDatabase(storageFilePath, { + logging: this.createLogginOptions() + }), { hint: wasCreated ? StorageHint.STORAGE_DOES_NOT_EXIST : undefined }); + + // Re-emit storage changes via event + this._register(storage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key }))); + + // Forward init to SQLite DB + await storage.init(); + + return storage; + } + + private async prepareWorkspaceStorageFolder(): Promise<{ storageFilePath: string, wasCreated: boolean }> { + + // Return early if using inMemory storage + if (this.options.useInMemoryStorage) { + return { storageFilePath: SQLiteStorageDatabase.IN_MEMORY_PATH, wasCreated: true }; + } + + // Otherwise, ensure the storage folder exists on disk + const workspaceStorageFolderPath = join(this.environmentService.workspaceStorageHome.fsPath, this.workspace.id); + const workspaceStorageDatabasePath = join(workspaceStorageFolderPath, WorkspaceStorageMain.WORKSPACE_STORAGE_NAME); + + const storageExists = await exists(workspaceStorageFolderPath); + if (storageExists) { + return { storageFilePath: workspaceStorageDatabasePath, wasCreated: false }; + } + + await promises.mkdir(workspaceStorageFolderPath, { recursive: true }); + + // Write metadata into folder + this.ensureWorkspaceStorageFolderMeta(workspaceStorageFolderPath); + + return { storageFilePath: workspaceStorageDatabasePath, wasCreated: true }; + } + + private ensureWorkspaceStorageFolderMeta(workspaceStorageFolderPath: string): void { + let meta: object | undefined = undefined; + if (isSingleFolderWorkspaceIdentifier(this.workspace)) { + meta = { folder: this.workspace.uri.toString() }; + } else if (isWorkspaceIdentifier(this.workspace)) { + meta = { workspace: this.workspace.configPath.toString() }; + } + + if (meta) { + (async () => { + try { + const workspaceStorageMetaPath = join(workspaceStorageFolderPath, WorkspaceStorageMain.WORKSPACE_META_NAME); + const storageExists = await exists(workspaceStorageMetaPath); + if (!storageExists) { + await writeFile(workspaceStorageMetaPath, JSON.stringify(meta, undefined, 2)); + } + } catch (error) { + this.logService.error(`StorageMain#ensureWorkspaceStorageFolderMeta(): Unable to create workspace storage metadata due to ${error}`); + } + })(); + } + } +} diff --git a/src/vs/platform/storage/electron-main/storageMainService.ts b/src/vs/platform/storage/electron-main/storageMainService.ts new file mode 100644 index 00000000000..6b50b94256a --- /dev/null +++ b/src/vs/platform/storage/electron-main/storageMainService.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { once } from 'vs/base/common/functional'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { GlobalStorageMain, IStorageMain, IStorageMainOptions, WorkspaceStorageMain } from 'vs/platform/storage/electron-main/storageMain'; +import { IWindowSettings } from 'vs/platform/windows/common/windows'; +import { IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; + +export const IStorageMainService = createDecorator('storageMainService'); + +export interface IStorageMainService { + + readonly _serviceBrand: undefined; + + /** + * Provides access to the global storage shared across all windows. + */ + readonly globalStorage: IStorageMain; + + /** + * Provides access to the workspace storage specific to a single window. + */ + workspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier): IStorageMain; +} + +export class StorageMainService extends Disposable implements IStorageMainService { + + declare readonly _serviceBrand: undefined; + + constructor( + @ILogService private readonly logService: ILogService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super(); + + this.registerListeners(); + } + + protected getStorageOptions(): IStorageMainOptions { + return { + useInMemoryStorage: !!this.environmentService.extensionTestsLocationURI // no storage during extension tests! + }; + } + + protected enableMainWorkspaceStorage(): boolean { + return !!(this.configurationService.getValue('window')?.enableExperimentalMainProcessWorkspaceStorage); + } + + private registerListeners(): void { + + // Global Storage: Warmup when any window opens + (async () => { + await this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen); + + this.globalStorage.initialize(); + })(); + + // Workspace Storage: Warmup when related window with workspace loads + if (this.enableMainWorkspaceStorage()) { + this._register(this.lifecycleMainService.onWillLoadWindow(async e => { + if (e.workspace) { + await this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen); + + this.workspaceStorage(e.workspace).initialize(); + } + })); + } + + // All Storage: Close when shutting down + this._register(this.lifecycleMainService.onWillShutdown(e => { + + // Global Storage + e.join(this.globalStorage.close()); + + // Workspace Storage(s) + for (const [, storage] of this.mapWorkspaceToStorage) { + e.join(storage.close()); + } + })); + } + + //#region Global Storage + + readonly globalStorage = this.createGlobalStorage(); + + private createGlobalStorage(): IStorageMain { + if (this.globalStorage) { + return this.globalStorage; // only once + } + + this.logService.trace(`StorageMainService: creating global storage`); + + const globalStorage = new GlobalStorageMain(this.getStorageOptions(), this.logService, this.environmentService); + + once(globalStorage.onDidCloseStorage)(() => { + this.logService.trace(`StorageMainService: closed global storage`); + }); + + return globalStorage; + } + + //#endregion + + + //#region Workspace Storage + + private readonly mapWorkspaceToStorage = new Map(); + + private createWorkspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier): IStorageMain { + const workspaceStorage = new WorkspaceStorageMain(workspace, this.getStorageOptions(), this.logService, this.environmentService); + + return workspaceStorage; + } + + workspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier): IStorageMain { + let workspaceStorage = this.mapWorkspaceToStorage.get(workspace.id); + if (!workspaceStorage) { + this.logService.trace(`StorageMainService: creating workspace storage (${workspace.id})`); + + workspaceStorage = this.createWorkspaceStorage(workspace); + this.mapWorkspaceToStorage.set(workspace.id, workspaceStorage); + + once(workspaceStorage.onDidCloseStorage)(() => { + this.logService.trace(`StorageMainService: closed workspace storage (${workspace.id})`); + + this.mapWorkspaceToStorage.delete(workspace.id); + }); + } + + return workspaceStorage; + } + + //#endregion +} diff --git a/src/vs/platform/storage/electron-sandbox/storageService2.ts b/src/vs/platform/storage/electron-sandbox/storageService2.ts new file mode 100644 index 00000000000..c9bbecee81c --- /dev/null +++ b/src/vs/platform/storage/electron-sandbox/storageService2.ts @@ -0,0 +1,134 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; +import { StorageScope, WillSaveStateReason, AbstractStorageService } from 'vs/platform/storage/common/storage'; +import { Storage, IStorage } from 'vs/base/parts/storage/common/storage'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; +import { Promises, RunOnceScheduler, runWhenIdle } from 'vs/base/common/async'; +import { mark } from 'vs/base/common/performance'; +import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; +import { StorageDatabaseChannelClient } from 'vs/platform/storage/common/storageIpc'; +import { joinPath } from 'vs/base/common/resources'; + +export class NativeStorageService2 extends AbstractStorageService { + + private readonly globalStorage: IStorage; + private readonly workspaceStorage: IStorage | undefined; + + private initializePromise: Promise | undefined; + + private readonly periodicFlushScheduler = this._register(new RunOnceScheduler(() => this.doFlushWhenIdle(), 60 * 1000 /* every minute */)); + private runWhenIdleDisposable: IDisposable | undefined = undefined; + + constructor( + private workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined, + mainProcessService: IMainProcessService, + private readonly environmentService: IEnvironmentService + ) { + super(); + + // Connect to storage via channel client + const storageDataBaseClient = new StorageDatabaseChannelClient(mainProcessService.getChannel('storage'), workspace); + this.globalStorage = new Storage(storageDataBaseClient.globalStorage); + this.workspaceStorage = storageDataBaseClient.workspaceStorage ? new Storage(storageDataBaseClient.workspaceStorage) : undefined; + + this.registerListeners(); + } + + private registerListeners(): void { + this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key))); + this._register(this.workspaceStorage?.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key)) ?? Disposable.None); + } + + initialize(): Promise { + if (!this.initializePromise) { + this.initializePromise = this.doInitialize(); + } + + return this.initializePromise; + } + + private async doInitialize(): Promise { + + // Init all storage locations + mark('code/willInitStorage'); + try { + await Promises.settled([ + this.globalStorage.init(), + this.workspaceStorage?.init() ?? Promise.resolve() + ]); + } finally { + mark('code/didInitStorage'); + } + + // On some OS we do not get enough time to persist state on shutdown (e.g. when + // Windows restarts after applying updates). In other cases, VSCode might crash, + // so we periodically save state to reduce the chance of loosing any state. + this.periodicFlushScheduler.schedule(); + } + + protected getStorage(scope: StorageScope): IStorage | undefined { + return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage; + } + + protected getLogDetails(scope: StorageScope): string | undefined { + return scope === StorageScope.GLOBAL ? this.environmentService.globalStorageHome.fsPath : this.workspace ? `${joinPath(this.environmentService.workspaceStorageHome, this.workspace.id, 'state.vscdb').fsPath} [!!! Experimental Main Storage !!!]` : undefined; + } + + private doFlushWhenIdle(): void { + + // Dispose any previous idle runner + dispose(this.runWhenIdleDisposable); + + // Run when idle + this.runWhenIdleDisposable = runWhenIdle(() => { + + // send event to collect state + this.flush(); + + // repeat + this.periodicFlushScheduler.schedule(); + }); + } + + async close(): Promise { + + // Stop periodic scheduler and idle runner as we now collect state normally + this.periodicFlushScheduler.dispose(); + dispose(this.runWhenIdleDisposable); + this.runWhenIdleDisposable = undefined; + + // Signal as event so that clients can still store data + this.emitWillSaveState(WillSaveStateReason.SHUTDOWN); + + // Do it + await Promises.settled([ + this.globalStorage.close(), + this.workspaceStorage?.close() ?? Promise.resolve() + ]); + } + + async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise { + // if (this.workspaceStoragePath === SQLiteStorageDatabase.IN_MEMORY_PATH) { + // return; // no migration needed if running in memory + // } + + // // Close workspace DB to be able to copy + // await this.getStorage(StorageScope.WORKSPACE).close(); + + // // Prepare new workspace storage folder + // const result = await this.prepareWorkspaceStorageFolder(toWorkspace); + + // const newWorkspaceStoragePath = join(result.path, NativeStorageService.WORKSPACE_STORAGE_NAME); + + // // Copy current storage over to new workspace storage + // await copy(assertIsDefined(this.workspaceStoragePath), newWorkspaceStoragePath, { preserveSymlinks: false }); + + // // Recreate and init workspace storage + // return this.createWorkspaceStorage(newWorkspaceStoragePath).init(); + } +} diff --git a/src/vs/platform/storage/node/storageIpc.ts b/src/vs/platform/storage/node/storageIpc.ts deleted file mode 100644 index 67195995c28..00000000000 --- a/src/vs/platform/storage/node/storageIpc.ts +++ /dev/null @@ -1,214 +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 { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { Event, Emitter } from 'vs/base/common/event'; -import { IStorageChangeEvent, IStorageMainService } from 'vs/platform/storage/node/storageMainService'; -import { IUpdateRequest, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/parts/storage/common/storage'; -import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { ILogService } from 'vs/platform/log/common/log'; -import { generateUuid } from 'vs/base/common/uuid'; -import { instanceStorageKey, firstSessionDateStorageKey, lastSessionDateStorageKey, currentSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; - -type Key = string; -type Value = string; -type Item = [Key, Value]; - -interface ISerializableUpdateRequest { - insert?: Item[]; - delete?: Key[]; -} - -interface ISerializableItemsChangeEvent { - readonly changed?: Item[]; - readonly deleted?: Key[]; -} - -export class GlobalStorageDatabaseChannel extends Disposable implements IServerChannel { - - private static readonly STORAGE_CHANGE_DEBOUNCE_TIME = 100; - - private readonly _onDidChangeItems = this._register(new Emitter()); - readonly onDidChangeItems = this._onDidChangeItems.event; - - private readonly whenReady = this.init(); - - constructor( - private logService: ILogService, - private storageMainService: IStorageMainService - ) { - super(); - } - - private async init(): Promise { - try { - await this.storageMainService.initialize(); - } catch (error) { - this.logService.error(`[storage] init(): Unable to init global storage due to ${error}`); - } - - // Apply global telemetry values as part of the initialization - // These are global across all windows and thereby should be - // written from the main process once. - this.initTelemetry(); - - // Setup storage change listeners - this.registerListeners(); - } - - private initTelemetry(): void { - const instanceId = this.storageMainService.get(instanceStorageKey, undefined); - if (instanceId === undefined) { - this.storageMainService.store(instanceStorageKey, generateUuid()); - } - - const firstSessionDate = this.storageMainService.get(firstSessionDateStorageKey, undefined); - if (firstSessionDate === undefined) { - this.storageMainService.store(firstSessionDateStorageKey, new Date().toUTCString()); - } - - const lastSessionDate = this.storageMainService.get(currentSessionDateStorageKey, undefined); // previous session date was the "current" one at that time - const currentSessionDate = new Date().toUTCString(); // current session date is "now" - this.storageMainService.store(lastSessionDateStorageKey, typeof lastSessionDate === 'undefined' ? null : lastSessionDate); - this.storageMainService.store(currentSessionDateStorageKey, currentSessionDate); - } - - private registerListeners(): void { - - // Listen for changes in global storage to send to listeners - // that are listening. Use a debouncer to reduce IPC traffic. - this._register(Event.debounce(this.storageMainService.onDidChangeStorage, (prev: IStorageChangeEvent[] | undefined, cur: IStorageChangeEvent) => { - if (!prev) { - prev = [cur]; - } else { - prev.push(cur); - } - - return prev; - }, GlobalStorageDatabaseChannel.STORAGE_CHANGE_DEBOUNCE_TIME)(events => { - if (events.length) { - this._onDidChangeItems.fire(this.serializeEvents(events)); - } - })); - } - - private serializeEvents(events: IStorageChangeEvent[]): ISerializableItemsChangeEvent { - const changed = new Map(); - const deleted = new Set(); - events.forEach(event => { - const existing = this.storageMainService.get(event.key); - if (typeof existing === 'string') { - changed.set(event.key, existing); - } else { - deleted.add(event.key); - } - }); - - return { - changed: Array.from(changed.entries()), - deleted: Array.from(deleted.values()) - }; - } - - listen(_: unknown, event: string): Event { - switch (event) { - case 'onDidChangeItems': return this.onDidChangeItems; - } - - throw new Error(`Event not found: ${event}`); - } - - async call(_: unknown, command: string, arg?: any): Promise { - - // ensure to always wait for ready - await this.whenReady; - - // handle call - switch (command) { - case 'getItems': { - return Array.from(this.storageMainService.items.entries()); - } - - case 'updateItems': { - const items: ISerializableUpdateRequest = arg; - if (items.insert) { - for (const [key, value] of items.insert) { - this.storageMainService.store(key, value); - } - } - - if (items.delete) { - items.delete.forEach(key => this.storageMainService.remove(key)); - } - - break; - } - - default: - throw new Error(`Call not found: ${command}`); - } - } -} - -export class GlobalStorageDatabaseChannelClient extends Disposable implements IStorageDatabase { - - declare readonly _serviceBrand: undefined; - - private readonly _onDidChangeItemsExternal = this._register(new Emitter()); - readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event; - - private onDidChangeItemsOnMainListener: IDisposable | undefined; - - constructor(private channel: IChannel) { - super(); - - this.registerListeners(); - } - - private registerListeners(): void { - this.onDidChangeItemsOnMainListener = this.channel.listen('onDidChangeItems')((e: ISerializableItemsChangeEvent) => this.onDidChangeItemsOnMain(e)); - } - - private onDidChangeItemsOnMain(e: ISerializableItemsChangeEvent): void { - if (Array.isArray(e.changed) || Array.isArray(e.deleted)) { - this._onDidChangeItemsExternal.fire({ - changed: e.changed ? new Map(e.changed) : undefined, - deleted: e.deleted ? new Set(e.deleted) : undefined - }); - } - } - - async getItems(): Promise> { - const items: Item[] = await this.channel.call('getItems'); - - return new Map(items); - } - - updateItems(request: IUpdateRequest): Promise { - const serializableRequest: ISerializableUpdateRequest = Object.create(null); - - if (request.insert) { - serializableRequest.insert = Array.from(request.insert.entries()); - } - - if (request.delete) { - serializableRequest.delete = Array.from(request.delete.values()); - } - - return this.channel.call('updateItems', serializableRequest); - } - - async close(): Promise { - - // when we are about to close, we start to ignore main-side changes since we close anyway - dispose(this.onDidChangeItemsOnMainListener); - } - - dispose(): void { - super.dispose(); - - dispose(this.onDidChangeItemsOnMainListener); - } -} diff --git a/src/vs/platform/storage/node/storageMainService.ts b/src/vs/platform/storage/node/storageMainService.ts deleted file mode 100644 index 0d25d4e6554..00000000000 --- a/src/vs/platform/storage/node/storageMainService.ts +++ /dev/null @@ -1,191 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { Event, Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { ILogService, LogLevel } from 'vs/platform/log/common/log'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage'; -import { Storage, IStorage, InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage'; -import { join } from 'vs/base/common/path'; -import { IS_NEW_KEY } from 'vs/platform/storage/common/storage'; - -export const IStorageMainService = createDecorator('storageMainService'); - -export interface IStorageMainService { - - readonly _serviceBrand: undefined; - - /** - * Emitted whenever data is updated or deleted. - */ - readonly onDidChangeStorage: Event; - - /** - * Emitted when the storage is about to persist. This is the right time - * to persist data to ensure it is stored before the application shuts - * down. - * - * Note: this event may be fired many times, not only on shutdown to prevent - * loss of state in situations where the shutdown is not sufficient to - * persist the data properly. - */ - readonly onWillSaveState: Event; - - /** - * Access to all cached items of this storage service. - */ - readonly items: Map; - - /** - * Required call to ensure the service can be used. - */ - initialize(): Promise; - - /** - * Retrieve an element stored with the given key from storage. Use - * the provided defaultValue if the element is null or undefined. - */ - get(key: string, fallbackValue: string): string; - get(key: string, fallbackValue?: string): string | undefined; - - /** - * Retrieve an element stored with the given key from storage. Use - * the provided defaultValue if the element is null or undefined. The element - * will be converted to a boolean. - */ - getBoolean(key: string, fallbackValue: boolean): boolean; - getBoolean(key: string, fallbackValue?: boolean): boolean | undefined; - - /** - * Retrieve an element stored with the given key from storage. Use - * the provided defaultValue if the element is null or undefined. The element - * will be converted to a number using parseInt with a base of 10. - */ - getNumber(key: string, fallbackValue: number): number; - getNumber(key: string, fallbackValue?: number): number | undefined; - - /** - * Store a string value under the given key to storage. The value will - * be converted to a string. - */ - store(key: string, value: string | boolean | number | undefined | null): void; - - /** - * Delete an element stored under the provided key from storage. - */ - remove(key: string): void; -} - -export interface IStorageChangeEvent { - key: string; -} - -export class StorageMainService extends Disposable implements IStorageMainService { - - declare readonly _serviceBrand: undefined; - - private static readonly STORAGE_NAME = 'state.vscdb'; - - private readonly _onDidChangeStorage = this._register(new Emitter()); - readonly onDidChangeStorage = this._onDidChangeStorage.event; - - private readonly _onWillSaveState = this._register(new Emitter()); - readonly onWillSaveState = this._onWillSaveState.event; - - get items(): Map { return this.storage.items; } - - private storage: IStorage; - - private initializePromise: Promise | undefined; - - constructor( - @ILogService private readonly logService: ILogService, - @IEnvironmentService private readonly environmentService: IEnvironmentService - ) { - super(); - - // Until the storage has been initialized, it can only be in memory - this.storage = new Storage(new InMemoryStorageDatabase()); - } - - private get storagePath(): string { - if (!!this.environmentService.extensionTestsLocationURI) { - return SQLiteStorageDatabase.IN_MEMORY_PATH; // no storage during extension tests! - } - - return join(this.environmentService.globalStorageHome.fsPath, StorageMainService.STORAGE_NAME); - } - - private createLogginOptions(): ISQLiteStorageDatabaseLoggingOptions { - return { - logTrace: (this.logService.getLevel() === LogLevel.Trace) ? msg => this.logService.trace(msg) : undefined, - logError: error => this.logService.error(error) - }; - } - - initialize(): Promise { - if (!this.initializePromise) { - this.initializePromise = this.doInitialize(); - } - - return this.initializePromise; - } - - private async doInitialize(): Promise { - this.storage.dispose(); - this.storage = new Storage(new SQLiteStorageDatabase(this.storagePath, { - logging: this.createLogginOptions() - })); - - this._register(this.storage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key }))); - - await this.storage.init(); - - // Check to see if this is the first time we are "opening" the application - const firstOpen = this.storage.getBoolean(IS_NEW_KEY); - if (firstOpen === undefined) { - this.storage.set(IS_NEW_KEY, true); - } else if (firstOpen) { - this.storage.set(IS_NEW_KEY, false); - } - } - - get(key: string, fallbackValue: string): string; - get(key: string, fallbackValue?: string): string | undefined; - get(key: string, fallbackValue?: string): string | undefined { - return this.storage.get(key, fallbackValue); - } - - getBoolean(key: string, fallbackValue: boolean): boolean; - getBoolean(key: string, fallbackValue?: boolean): boolean | undefined; - getBoolean(key: string, fallbackValue?: boolean): boolean | undefined { - return this.storage.getBoolean(key, fallbackValue); - } - - getNumber(key: string, fallbackValue: number): number; - getNumber(key: string, fallbackValue?: number): number | undefined; - getNumber(key: string, fallbackValue?: number): number | undefined { - return this.storage.getNumber(key, fallbackValue); - } - - store(key: string, value: string | boolean | number | undefined | null): Promise { - return this.storage.set(key, value); - } - - remove(key: string): Promise { - return this.storage.delete(key); - } - - close(): Promise { - - // Signal as event so that clients can still store data - this._onWillSaveState.fire(); - - // Do it - return this.storage.close(); - } -} diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts index 111bbf4874c..316263b1b52 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts @@ -6,7 +6,7 @@ import { promises } from 'fs'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; -import { StorageScope, WillSaveStateReason, logStorage, IS_NEW_KEY, AbstractStorageService } from 'vs/platform/storage/common/storage'; +import { StorageScope, WillSaveStateReason, IS_NEW_KEY, AbstractStorageService } from 'vs/platform/storage/common/storage'; import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage'; import { Storage, IStorageDatabase, IStorage, StorageHint } from 'vs/base/parts/storage/common/storage'; import { mark } from 'vs/base/common/performance'; @@ -170,47 +170,12 @@ export class NativeStorageService extends AbstractStorageService { } } - get(key: string, scope: StorageScope, fallbackValue: string): string; - get(key: string, scope: StorageScope): string | undefined; - get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined { - return this.getStorage(scope).get(key, fallbackValue); + protected getStorage(scope: StorageScope): IStorage | undefined { + return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage; } - getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean; - getBoolean(key: string, scope: StorageScope): boolean | undefined; - getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined { - return this.getStorage(scope).getBoolean(key, fallbackValue); - } - - getNumber(key: string, scope: StorageScope, fallbackValue: number): number; - getNumber(key: string, scope: StorageScope): number | undefined; - getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined { - return this.getStorage(scope).getNumber(key, fallbackValue); - } - - protected doStore(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): void { - this.getStorage(scope).set(key, value); - } - - protected doRemove(key: string, scope: StorageScope): void { - this.getStorage(scope).delete(key); - } - - private getStorage(scope: StorageScope): IStorage { - return assertIsDefined(scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage); - } - - protected async doFlush(): Promise { - const promises: Promise[] = []; - if (this.globalStorage) { - promises.push(this.globalStorage.whenFlushed()); - } - - if (this.workspaceStorage) { - promises.push(this.workspaceStorage.whenFlushed()); - } - - await Promises.settled(promises); + protected getLogDetails(scope: StorageScope): string | undefined { + return scope === StorageScope.GLOBAL ? this.environmentService.globalStorageHome.fsPath : this.workspaceStoragePath; } private doFlushWhenIdle(): void { @@ -246,21 +211,13 @@ export class NativeStorageService extends AbstractStorageService { ]); } - async logStorage(): Promise { - return logStorage( - this.globalStorage.items, - this.workspaceStorage ? this.workspaceStorage.items : new Map(), // Shared process storage does not has workspace storage - this.environmentService.globalStorageHome.fsPath, - this.workspaceStoragePath || ''); - } - async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise { if (this.workspaceStoragePath === SQLiteStorageDatabase.IN_MEMORY_PATH) { return; // no migration needed if running in memory } // Close workspace DB to be able to copy - await this.getStorage(StorageScope.WORKSPACE).close(); + await this.workspaceStorage?.close(); // Prepare new workspace storage folder const result = await this.prepareWorkspaceStorageFolder(toWorkspace); diff --git a/src/vs/platform/storage/test/electron-browser/storage.test.ts b/src/vs/platform/storage/test/browser/storageService.test.ts similarity index 54% rename from src/vs/platform/storage/test/electron-browser/storage.test.ts rename to src/vs/platform/storage/test/browser/storageService.test.ts index 836f287788c..fdeb01f61c6 100644 --- a/src/vs/platform/storage/test/electron-browser/storage.test.ts +++ b/src/vs/platform/storage/test/browser/storageService.test.ts @@ -4,25 +4,45 @@ *--------------------------------------------------------------------------------------------*/ import { strictEqual } from 'assert'; -import { FileStorageDatabase } from 'vs/platform/storage/browser/storageService'; -import { join } from 'vs/base/common/path'; -import { tmpdir } from 'os'; -import { rimraf } from 'vs/base/node/pfs'; +import { BrowserStorageService, FileStorageDatabase } from 'vs/platform/storage/browser/storageService'; import { NullLogService } from 'vs/platform/log/common/log'; import { Storage } from 'vs/base/parts/storage/common/storage'; import { URI } from 'vs/base/common/uri'; import { FileService } from 'vs/platform/files/common/fileService'; -import { getRandomTestPath } from 'vs/base/test/node/testUtils'; -import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { createSuite } from 'vs/platform/storage/test/common/storageService.test'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -suite('Storage', () => { +suite('StorageService (browser)', function () { - let testDir: string; + const disposables = new DisposableStore(); + + createSuite({ + setup: async () => { + const logService = new NullLogService(); + + const fileService = disposables.add(new FileService(logService)); + + const userDataProvider = disposables.add(new InMemoryFileSystemProvider()); + disposables.add(fileService.registerProvider(Schemas.userData, userDataProvider)); + + const storageService = disposables.add(new BrowserStorageService({ userRoamingDataHome: URI.file('/User').with({ scheme: Schemas.userData }) } as unknown as IEnvironmentService, fileService)); + + await storageService.initialize({ id: String(Date.now()) }); + + return storageService; + }, + teardown: async storage => { + disposables.clear(); + } + }); +}); + +suite('FileStorageDatabase (browser)', () => { let fileService: FileService; - let fileProvider: DiskFileSystemProvider; const disposables = new DisposableStore(); @@ -31,20 +51,18 @@ suite('Storage', () => { fileService = disposables.add(new FileService(logService)); - fileProvider = disposables.add(new DiskFileSystemProvider(logService)); - disposables.add(fileService.registerProvider(Schemas.file, fileProvider)); - - testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice'); + const userDataProvider = disposables.add(new InMemoryFileSystemProvider()); + disposables.add(fileService.registerProvider(Schemas.userData, userDataProvider)); }); teardown(() => { disposables.clear(); - - return rimraf(testDir); }); - test('File Based Storage', async () => { - let storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), false, fileService)); + test('Basics', async () => { + const testDir = URI.file('/User/storage.json').with({ scheme: Schemas.userData }); + + let storage = new Storage(new FileStorageDatabase(testDir, false, fileService)); await storage.init(); @@ -58,7 +76,7 @@ suite('Storage', () => { await storage.close(); - storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), false, fileService)); + storage = new Storage(new FileStorageDatabase(testDir, false, fileService)); await storage.init(); @@ -76,7 +94,7 @@ suite('Storage', () => { await storage.close(); - storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), false, fileService)); + storage = new Storage(new FileStorageDatabase(testDir, false, fileService)); await storage.init(); diff --git a/src/vs/platform/storage/test/common/storageService.test.ts b/src/vs/platform/storage/test/common/storageService.test.ts index 182ff12af03..fd115f627de 100644 --- a/src/vs/platform/storage/test/common/storageService.test.ts +++ b/src/vs/platform/storage/test/common/storageService.test.ts @@ -4,98 +4,102 @@ *--------------------------------------------------------------------------------------------*/ import { strictEqual, ok } from 'assert'; -import { StorageScope, InMemoryStorageService, StorageTarget, IStorageValueChangeEvent, IStorageTargetChangeEvent } from 'vs/platform/storage/common/storage'; +import { StorageScope, InMemoryStorageService, StorageTarget, IStorageValueChangeEvent, IStorageTargetChangeEvent, IStorageService } from 'vs/platform/storage/common/storage'; -suite('StorageService', function () { +export function createSuite(params: { setup: () => Promise, teardown: (service: T) => Promise }): void { - test('Get Data, Integer, Boolean (global, in-memory)', () => { + let storageService: T; + + setup(async () => { + storageService = await params.setup(); + }); + + teardown(() => { + return params.teardown(storageService); + }); + + test('Get Data, Integer, Boolean (global)', () => { storeData(StorageScope.GLOBAL); }); - test('Get Data, Integer, Boolean (workspace, in-memory)', () => { + test('Get Data, Integer, Boolean (workspace)', () => { storeData(StorageScope.WORKSPACE); }); function storeData(scope: StorageScope): void { - const storage = new InMemoryStorageService(); - let storageValueChangeEvents: IStorageValueChangeEvent[] = []; - storage.onDidChangeValue(e => storageValueChangeEvents.push(e)); + storageService.onDidChangeValue(e => storageValueChangeEvents.push(e)); - strictEqual(storage.get('test.get', scope, 'foobar'), 'foobar'); - strictEqual(storage.get('test.get', scope, ''), ''); - strictEqual(storage.getNumber('test.getNumber', scope, 5), 5); - strictEqual(storage.getNumber('test.getNumber', scope, 0), 0); - strictEqual(storage.getBoolean('test.getBoolean', scope, true), true); - strictEqual(storage.getBoolean('test.getBoolean', scope, false), false); + strictEqual(storageService.get('test.get', scope, 'foobar'), 'foobar'); + strictEqual(storageService.get('test.get', scope, ''), ''); + strictEqual(storageService.getNumber('test.getNumber', scope, 5), 5); + strictEqual(storageService.getNumber('test.getNumber', scope, 0), 0); + strictEqual(storageService.getBoolean('test.getBoolean', scope, true), true); + strictEqual(storageService.getBoolean('test.getBoolean', scope, false), false); - storage.store('test.get', 'foobar', scope, StorageTarget.MACHINE); - strictEqual(storage.get('test.get', scope, (undefined)!), 'foobar'); + storageService.store('test.get', 'foobar', scope, StorageTarget.MACHINE); + strictEqual(storageService.get('test.get', scope, (undefined)!), 'foobar'); let storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.get'); strictEqual(storageValueChangeEvent?.scope, scope); strictEqual(storageValueChangeEvent?.key, 'test.get'); storageValueChangeEvents = []; - storage.store('test.get', '', scope, StorageTarget.MACHINE); - strictEqual(storage.get('test.get', scope, (undefined)!), ''); + storageService.store('test.get', '', scope, StorageTarget.MACHINE); + strictEqual(storageService.get('test.get', scope, (undefined)!), ''); storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.get'); strictEqual(storageValueChangeEvent!.scope, scope); strictEqual(storageValueChangeEvent!.key, 'test.get'); - storage.store('test.getNumber', 5, scope, StorageTarget.MACHINE); - strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 5); + storageService.store('test.getNumber', 5, scope, StorageTarget.MACHINE); + strictEqual(storageService.getNumber('test.getNumber', scope, (undefined)!), 5); - storage.store('test.getNumber', 0, scope, StorageTarget.MACHINE); - strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 0); + storageService.store('test.getNumber', 0, scope, StorageTarget.MACHINE); + strictEqual(storageService.getNumber('test.getNumber', scope, (undefined)!), 0); - storage.store('test.getBoolean', true, scope, StorageTarget.MACHINE); - strictEqual(storage.getBoolean('test.getBoolean', scope, (undefined)!), true); + storageService.store('test.getBoolean', true, scope, StorageTarget.MACHINE); + strictEqual(storageService.getBoolean('test.getBoolean', scope, (undefined)!), true); - storage.store('test.getBoolean', false, scope, StorageTarget.MACHINE); - strictEqual(storage.getBoolean('test.getBoolean', scope, (undefined)!), false); + storageService.store('test.getBoolean', false, scope, StorageTarget.MACHINE); + strictEqual(storageService.getBoolean('test.getBoolean', scope, (undefined)!), false); - strictEqual(storage.get('test.getDefault', scope, 'getDefault'), 'getDefault'); - strictEqual(storage.getNumber('test.getNumberDefault', scope, 5), 5); - strictEqual(storage.getBoolean('test.getBooleanDefault', scope, true), true); + strictEqual(storageService.get('test.getDefault', scope, 'getDefault'), 'getDefault'); + strictEqual(storageService.getNumber('test.getNumberDefault', scope, 5), 5); + strictEqual(storageService.getBoolean('test.getBooleanDefault', scope, true), true); } - test('Remove Data (global, in-memory)', () => { + test('Remove Data (global)', () => { removeData(StorageScope.GLOBAL); }); - test('Remove Data (workspace, in-memory)', () => { + test('Remove Data (workspace)', () => { removeData(StorageScope.WORKSPACE); }); function removeData(scope: StorageScope): void { - const storage = new InMemoryStorageService(); - let storageValueChangeEvents: IStorageValueChangeEvent[] = []; - storage.onDidChangeValue(e => storageValueChangeEvents.push(e)); + storageService.onDidChangeValue(e => storageValueChangeEvents.push(e)); - storage.store('test.remove', 'foobar', scope, StorageTarget.MACHINE); - strictEqual('foobar', storage.get('test.remove', scope, (undefined)!)); + storageService.store('test.remove', 'foobar', scope, StorageTarget.MACHINE); + strictEqual('foobar', storageService.get('test.remove', scope, (undefined)!)); - storage.remove('test.remove', scope); - ok(!storage.get('test.remove', scope, (undefined)!)); + storageService.remove('test.remove', scope); + ok(!storageService.get('test.remove', scope, (undefined)!)); let storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.remove'); strictEqual(storageValueChangeEvent?.scope, scope); strictEqual(storageValueChangeEvent?.key, 'test.remove'); } test('Keys (in-memory)', () => { - const storage = new InMemoryStorageService(); - let storageTargetEvent: IStorageTargetChangeEvent | undefined = undefined; - storage.onDidChangeTarget(e => storageTargetEvent = e); + storageService.onDidChangeTarget(e => storageTargetEvent = e); let storageValueChangeEvent: IStorageValueChangeEvent | undefined = undefined; - storage.onDidChangeValue(e => storageValueChangeEvent = e); + storageService.onDidChangeValue(e => storageValueChangeEvent = e); // Empty for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) { for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) { - strictEqual(storage.keys(scope, target).length, 0); + strictEqual(storageService.keys(scope, target).length, 0); } } @@ -105,8 +109,8 @@ suite('StorageService', function () { storageTargetEvent = Object.create(null); storageValueChangeEvent = Object.create(null); - storage.store('test.target1', 'value1', scope, target); - strictEqual(storage.keys(scope, target).length, 1); + storageService.store('test.target1', 'value1', scope, target); + strictEqual(storageService.keys(scope, target).length, 1); strictEqual(storageTargetEvent?.scope, scope); strictEqual(storageValueChangeEvent?.key, 'test.target1'); strictEqual(storageValueChangeEvent?.scope, scope); @@ -115,33 +119,33 @@ suite('StorageService', function () { storageTargetEvent = undefined; storageValueChangeEvent = Object.create(null); - storage.store('test.target1', 'otherValue1', scope, target); - strictEqual(storage.keys(scope, target).length, 1); + storageService.store('test.target1', 'otherValue1', scope, target); + strictEqual(storageService.keys(scope, target).length, 1); strictEqual(storageTargetEvent, undefined); strictEqual(storageValueChangeEvent?.key, 'test.target1'); strictEqual(storageValueChangeEvent?.scope, scope); strictEqual(storageValueChangeEvent?.target, target); - storage.store('test.target2', 'value2', scope, target); - storage.store('test.target3', 'value3', scope, target); + storageService.store('test.target2', 'value2', scope, target); + storageService.store('test.target3', 'value3', scope, target); - strictEqual(storage.keys(scope, target).length, 3); + strictEqual(storageService.keys(scope, target).length, 3); } } // Remove values for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) { for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) { - const keysLength = storage.keys(scope, target).length; + const keysLength = storageService.keys(scope, target).length; - storage.store('test.target4', 'value1', scope, target); - strictEqual(storage.keys(scope, target).length, keysLength + 1); + storageService.store('test.target4', 'value1', scope, target); + strictEqual(storageService.keys(scope, target).length, keysLength + 1); storageTargetEvent = Object.create(null); storageValueChangeEvent = Object.create(null); - storage.remove('test.target4', scope); - strictEqual(storage.keys(scope, target).length, keysLength); + storageService.remove('test.target4', scope); + strictEqual(storageService.keys(scope, target).length, keysLength); strictEqual(storageTargetEvent?.scope, scope); strictEqual(storageValueChangeEvent?.key, 'test.target4'); strictEqual(storageValueChangeEvent?.scope, scope); @@ -151,48 +155,55 @@ suite('StorageService', function () { // Remove all for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) { for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) { - const keys = storage.keys(scope, target); + const keys = storageService.keys(scope, target); for (const key of keys) { - storage.remove(key, scope); + storageService.remove(key, scope); } - strictEqual(storage.keys(scope, target).length, 0); + strictEqual(storageService.keys(scope, target).length, 0); } } // Adding undefined or null removes value for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) { for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) { - storage.store('test.target1', 'value1', scope, target); - strictEqual(storage.keys(scope, target).length, 1); + storageService.store('test.target1', 'value1', scope, target); + strictEqual(storageService.keys(scope, target).length, 1); storageTargetEvent = Object.create(null); - storage.store('test.target1', undefined, scope, target); - strictEqual(storage.keys(scope, target).length, 0); + storageService.store('test.target1', undefined, scope, target); + strictEqual(storageService.keys(scope, target).length, 0); strictEqual(storageTargetEvent?.scope, scope); - storage.store('test.target1', '', scope, target); - strictEqual(storage.keys(scope, target).length, 1); + storageService.store('test.target1', '', scope, target); + strictEqual(storageService.keys(scope, target).length, 1); - storage.store('test.target1', null, scope, target); - strictEqual(storage.keys(scope, target).length, 0); + storageService.store('test.target1', null, scope, target); + strictEqual(storageService.keys(scope, target).length, 0); } } // Target change storageTargetEvent = undefined; - storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE); ok(storageTargetEvent); storageTargetEvent = undefined; - storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.USER); + storageService.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.USER); ok(storageTargetEvent); storageTargetEvent = undefined; - storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE); ok(storageTargetEvent); storageTargetEvent = undefined; - storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE); ok(!storageTargetEvent); // no change in target }); +} + +suite('StorageService (in-memory)', function () { + createSuite({ + setup: async () => new InMemoryStorageService(), + teardown: async () => { } + }); }); diff --git a/src/vs/platform/storage/test/electron-main/storageMainService.test.ts b/src/vs/platform/storage/test/electron-main/storageMainService.test.ts new file mode 100644 index 00000000000..3aa295edca7 --- /dev/null +++ b/src/vs/platform/storage/test/electron-main/storageMainService.test.ts @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { notStrictEqual, strictEqual } from 'assert'; +import { OPTIONS, parseArgs } from 'vs/platform/environment/node/argv'; +import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { StorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; +import { currentSessionDateStorageKey, firstSessionDateStorageKey, instanceStorageKey } from 'vs/platform/telemetry/common/telemetry'; +import { IStorageChangeEvent, IStorageMain, IStorageMainOptions } from 'vs/platform/storage/electron-main/storageMain'; +import { generateUuid } from 'vs/base/common/uuid'; +import { IS_NEW_KEY } from 'vs/platform/storage/common/storage'; +import { ILifecycleMainService, LifecycleMainPhase, ShutdownEvent, UnloadReason } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; +import { Emitter, Event } from 'vs/base/common/event'; +import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; +import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; +import { Promises, timeout } from 'vs/base/common/async'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; + +suite('StorageMainService (native)', function () { + + class TestStorageMainService extends StorageMainService { + + protected getStorageOptions(): IStorageMainOptions { + return { + useInMemoryStorage: true + }; + } + + protected enableMainWorkspaceStorage(): boolean { + return true; + } + } + + class StorageTestLifecycleMainService implements ILifecycleMainService { + + _serviceBrand: undefined; + + onBeforeShutdown = Event.None; + + private readonly _onWillShutdown = new Emitter(); + readonly onWillShutdown = this._onWillShutdown.event; + + async fireOnWillShutdown(): Promise { + const joiners: Promise[] = []; + + this._onWillShutdown.fire({ + join(promise) { + if (promise) { + joiners.push(promise); + } + } + }); + + await Promises.settled(joiners); + } + + onWillLoadWindow = Event.None; + onBeforeCloseWindow = Event.None; + onBeforeUnloadWindow = Event.None; + + wasRestarted = false; + quitRequested = false; + + phase = LifecycleMainPhase.Ready; + + registerWindow(window: ICodeWindow): void { } + async reload(window: ICodeWindow, cli?: NativeParsedArgs): Promise { } + async unload(window: ICodeWindow, reason: UnloadReason): Promise { return true; } + relaunch(options?: { addArgs?: string[] | undefined; removeArgs?: string[] | undefined; }): void { } + async quit(fromUpdate?: boolean): Promise { return true; } + async kill(code?: number): Promise { } + async when(phase: LifecycleMainPhase): Promise { } + } + + async function testStorage(storage: IStorageMain, isGlobal: boolean): Promise { + + // Telemetry: added after init + if (isGlobal) { + strictEqual(storage.items.size, 0); + strictEqual(storage.get(instanceStorageKey), undefined); + await storage.initialize(); + strictEqual(typeof storage.get(instanceStorageKey), 'string'); + strictEqual(typeof storage.get(firstSessionDateStorageKey), 'string'); + strictEqual(typeof storage.get(currentSessionDateStorageKey), 'string'); + } else { + await storage.initialize(); + } + + let storageChangeEvent: IStorageChangeEvent | undefined = undefined; + const storageChangeListener = storage.onDidChangeStorage(e => { + storageChangeEvent = e; + }); + + let storageDidClose = false; + const storageCloseListener = storage.onDidCloseStorage(() => storageDidClose = true); + + // Basic store/get/remove + const size = storage.items.size; + + storage.store('bar', 'foo'); + strictEqual(storageChangeEvent!.key, 'bar'); + storage.store('barNumber', 55); + storage.store('barBoolean', true); + + strictEqual(storage.get('bar'), 'foo'); + strictEqual(storage.getNumber('barNumber'), 55); + strictEqual(storage.getBoolean('barBoolean'), true); + + strictEqual(storage.items.size, size + 3); + + storage.remove('bar'); + strictEqual(storage.get('bar'), undefined); + + strictEqual(storage.items.size, size + 2); + + // IS_NEW + strictEqual(storage.getBoolean(IS_NEW_KEY), true); + + // Close + await storage.close(); + + strictEqual(storageDidClose, true); + + storageChangeListener.dispose(); + storageCloseListener.dispose(); + } + + test('basics (global)', function () { + const storageMainService = new TestStorageMainService(new NullLogService(), new NativeEnvironmentService(parseArgs(process.argv, OPTIONS)), new StorageTestLifecycleMainService(), new TestConfigurationService()); + + return testStorage(storageMainService.globalStorage, true); + }); + + test('basics (workspace)', function () { + const workspace = { id: generateUuid() }; + const storageMainService = new TestStorageMainService(new NullLogService(), new NativeEnvironmentService(parseArgs(process.argv, OPTIONS)), new StorageTestLifecycleMainService(), new TestConfigurationService()); + + return testStorage(storageMainService.workspaceStorage(workspace), false); + }); + + test('storage closed onWillShutdown', async function () { + const lifecycleMainService = new StorageTestLifecycleMainService(); + const workspace = { id: generateUuid() }; + const storageMainService = new TestStorageMainService(new NullLogService(), new NativeEnvironmentService(parseArgs(process.argv, OPTIONS)), lifecycleMainService, new TestConfigurationService()); + + let storage = storageMainService.workspaceStorage(workspace); + let didCloseStorage = false; + storage.onDidCloseStorage(() => { + didCloseStorage = true; + }); + + strictEqual(storage, storageMainService.workspaceStorage(workspace)); // same instance as long as not closed + + await storage.initialize(); + + await timeout(0); + await lifecycleMainService.fireOnWillShutdown(); + strictEqual(didCloseStorage, true); + + let storage2 = storageMainService.workspaceStorage(workspace); + notStrictEqual(storage, storage2); + + return storage2.close(); + }); +}); diff --git a/src/vs/platform/storage/test/node/storageService.test.ts b/src/vs/platform/storage/test/node/storageService.test.ts index fed89b3eda6..ce8eb321aac 100644 --- a/src/vs/platform/storage/test/node/storageService.test.ts +++ b/src/vs/platform/storage/test/node/storageService.test.ts @@ -15,38 +15,46 @@ import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; import { InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage'; import { URI } from 'vs/base/common/uri'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { createSuite } from 'vs/platform/storage/test/common/storageService.test'; -flakySuite('NativeStorageService', function () { +flakySuite('StorageService (native)', function () { + + class StorageTestEnvironmentService extends NativeEnvironmentService { + + constructor(private workspaceStorageFolderPath: URI, private _extensionsPath: string) { + super(parseArgs(process.argv, OPTIONS)); + } + + get workspaceStorageHome(): URI { + return this.workspaceStorageFolderPath; + } + + get extensionsPath(): string { + return this._extensionsPath; + } + } let testDir: string; - setup(() => { - testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice'); + createSuite({ + setup: async () => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice'); - return promises.mkdir(testDir, { recursive: true }); - }); + await promises.mkdir(testDir, { recursive: true }); - teardown(() => { - return rimraf(testDir); + const storageService = new NativeStorageService(new InMemoryStorageDatabase(), new NullLogService(), new StorageTestEnvironmentService(URI.file(testDir), testDir)); + await storageService.initialize({ id: String(Date.now()) }); + + return storageService; + }, + teardown: async storageService => { + await storageService.close(); + + return rimraf(testDir); + } }); test('Migrate Data', async function () { - - class StorageTestEnvironmentService extends NativeEnvironmentService { - - constructor(private workspaceStorageFolderPath: URI, private _extensionsPath: string) { - super(parseArgs(process.argv, OPTIONS)); - } - - get workspaceStorageHome(): URI { - return this.workspaceStorageFolderPath; - } - - get extensionsPath(): string { - return this._extensionsPath; - } - } - const storage = new NativeStorageService(new InMemoryStorageDatabase(), new NullLogService(), new StorageTestEnvironmentService(URI.file(testDir), testDir)); await storage.initialize({ id: String(Date.now()) }); diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 1256437a0d7..58b90b849ff 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -288,6 +288,7 @@ export const editorWidgetResizeBorder = registerColor('editorWidget.resizeBorder export const quickInputBackground = registerColor('quickInput.background', { dark: editorWidgetBackground, light: editorWidgetBackground, hc: editorWidgetBackground }, nls.localize('pickerBackground', "Quick picker background color. The quick picker widget is the container for pickers like the command palette.")); export const quickInputForeground = registerColor('quickInput.foreground', { dark: editorWidgetForeground, light: editorWidgetForeground, hc: editorWidgetForeground }, nls.localize('pickerForeground', "Quick picker foreground color. The quick picker widget is the container for pickers like the command palette.")); export const quickInputTitleBackground = registerColor('quickInputTitle.background', { dark: new Color(new RGBA(255, 255, 255, 0.105)), light: new Color(new RGBA(0, 0, 0, 0.06)), hc: '#000000' }, nls.localize('pickerTitleBackground', "Quick picker title background color. The quick picker widget is the container for pickers like the command palette.")); +export const quickInputListFocusBackground = registerColor('quickInput.list.focusBackground', { dark: '#062F4A', light: '#D6EBFF', hc: null }, nls.localize('quickInput.listFocusBackground', "Quick picker background color for the focused item.")); export const pickerGroupForeground = registerColor('pickerGroup.foreground', { dark: '#3794FF', light: '#0066BF', hc: Color.white }, nls.localize('pickerGroupForeground', "Quick picker color for grouping labels.")); export const pickerGroupBorder = registerColor('pickerGroup.border', { dark: '#3F3F46', light: '#CCCEDB', hc: Color.white }, nls.localize('pickerGroupBorder', "Quick picker color for grouping borders.")); @@ -362,13 +363,15 @@ export const diffDiagonalFill = registerColor('diffEditor.diagonalFill', { dark: /** * List and tree colors */ -export const listFocusBackground = registerColor('list.focusBackground', { dark: '#062F4A', light: '#D6EBFF', hc: null }, nls.localize('listFocusBackground', "List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); +export const listFocusBackground = registerColor('list.focusBackground', { dark: null, light: null, hc: null }, nls.localize('listFocusBackground', "List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listFocusForeground = registerColor('list.focusForeground', { dark: null, light: null, hc: null }, nls.localize('listFocusForeground', "List/Tree foreground color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); +export const listFocusOutline = registerColor('list.focusOutline', { dark: focusBorder, light: focusBorder, hc: activeContrastBorder }, nls.localize('listFocusOutline', "List/Tree outline color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listActiveSelectionBackground = registerColor('list.activeSelectionBackground', { dark: '#094771', light: '#0060C0', hc: null }, nls.localize('listActiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listActiveSelectionForeground = registerColor('list.activeSelectionForeground', { dark: Color.white, light: Color.white, hc: null }, nls.localize('listActiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveSelectionBackground = registerColor('list.inactiveSelectionBackground', { dark: '#37373D', light: '#E4E6F1', hc: null }, nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveSelectionForeground = registerColor('list.inactiveSelectionForeground', { dark: null, light: null, hc: null }, nls.localize('listInactiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveFocusBackground = registerColor('list.inactiveFocusBackground', { dark: null, light: null, hc: null }, nls.localize('listInactiveFocusBackground', "List/Tree background color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); +export const listInactiveFocusOutline = registerColor('list.inactiveFocusOutline', { dark: null, light: null, hc: null }, nls.localize('listInactiveFocusOutline', "List/Tree outline color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listHoverBackground = registerColor('list.hoverBackground', { dark: '#2A2D2E', light: '#F0F0F0', hc: null }, nls.localize('listHoverBackground', "List/Tree background when hovering over items using the mouse.")); export const listHoverForeground = registerColor('list.hoverForeground', { dark: null, light: null, hc: null }, nls.localize('listHoverForeground', "List/Tree foreground when hovering over items using the mouse.")); export const listDropBackground = registerColor('list.dropBackground', { dark: listFocusBackground, light: listFocusBackground, hc: null }, nls.localize('listDropBackground', "List/Tree drag and drop background when moving items around using the mouse.")); diff --git a/src/vs/platform/theme/common/styler.ts b/src/vs/platform/theme/common/styler.ts index 90bb201972c..02c66cc4325 100644 --- a/src/vs/platform/theme/common/styler.ts +++ b/src/vs/platform/theme/common/styler.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, badgeBackground, badgeForeground, progressBarBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground, editorWidgetBorder, inputValidationInfoForeground, inputValidationWarningForeground, inputValidationErrorForeground, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuBorder, menuSeparatorBackground, darken, listFilterWidgetOutline, listFilterWidgetNoMatchesOutline, listFilterWidgetBackground, editorWidgetBackground, treeIndentGuidesStroke, editorWidgetForeground, simpleCheckboxBackground, simpleCheckboxBorder, simpleCheckboxForeground, ColorValue, resolveColorValue, textLinkForeground, problemsWarningIconForeground, problemsErrorIconForeground, problemsInfoIconForeground, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground } from 'vs/platform/theme/common/colorRegistry'; +import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, badgeBackground, badgeForeground, progressBarBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground, editorWidgetBorder, inputValidationInfoForeground, inputValidationWarningForeground, inputValidationErrorForeground, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuBorder, menuSeparatorBackground, darken, listFilterWidgetOutline, listFilterWidgetNoMatchesOutline, listFilterWidgetBackground, editorWidgetBackground, treeIndentGuidesStroke, editorWidgetForeground, simpleCheckboxBackground, simpleCheckboxBorder, simpleCheckboxForeground, ColorValue, resolveColorValue, textLinkForeground, problemsWarningIconForeground, problemsErrorIconForeground, problemsInfoIconForeground, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground, listFocusOutline, listInactiveFocusOutline } from 'vs/platform/theme/common/colorRegistry'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Color } from 'vs/base/common/color'; import { IThemable, styleFn } from 'vs/base/common/styler'; @@ -210,6 +210,7 @@ export interface IListStyleOverrides extends IStyleOverrides { listBackground?: ColorIdentifier; listFocusBackground?: ColorIdentifier; listFocusForeground?: ColorIdentifier; + listFocusOutline?: ColorIdentifier; listActiveSelectionBackground?: ColorIdentifier; listActiveSelectionForeground?: ColorIdentifier; listFocusAndSelectionBackground?: ColorIdentifier; @@ -217,11 +218,10 @@ export interface IListStyleOverrides extends IStyleOverrides { listInactiveSelectionBackground?: ColorIdentifier; listInactiveSelectionForeground?: ColorIdentifier; listInactiveFocusBackground?: ColorIdentifier; + listInactiveFocusOutline?: ColorIdentifier; listHoverBackground?: ColorIdentifier; listHoverForeground?: ColorIdentifier; listDropBackground?: ColorIdentifier; - listFocusOutline?: ColorIdentifier; - listInactiveFocusOutline?: ColorIdentifier; listSelectionOutline?: ColorIdentifier; listHoverOutline?: ColorIdentifier; listFilterWidgetBackground?: ColorIdentifier; @@ -236,26 +236,27 @@ export function attachListStyler(widget: IThemable, themeService: IThemeService, } export const defaultListStyles: IColorMapping = { - listFocusBackground: listFocusBackground, - listFocusForeground: listFocusForeground, - listActiveSelectionBackground: darken(listActiveSelectionBackground, 0.1), - listActiveSelectionForeground: listActiveSelectionForeground, + listFocusBackground, + listFocusForeground, + listFocusOutline, + listActiveSelectionBackground, + listActiveSelectionForeground, listFocusAndSelectionBackground: listActiveSelectionBackground, listFocusAndSelectionForeground: listActiveSelectionForeground, - listInactiveSelectionBackground: listInactiveSelectionBackground, - listInactiveSelectionForeground: listInactiveSelectionForeground, - listInactiveFocusBackground: listInactiveFocusBackground, - listHoverBackground: listHoverBackground, - listHoverForeground: listHoverForeground, - listDropBackground: listDropBackground, - listFocusOutline: activeContrastBorder, + listInactiveSelectionBackground, + listInactiveSelectionForeground, + listInactiveFocusBackground, + listInactiveFocusOutline, + listHoverBackground, + listHoverForeground, + listDropBackground, listSelectionOutline: activeContrastBorder, listHoverOutline: activeContrastBorder, - listFilterWidgetBackground: listFilterWidgetBackground, - listFilterWidgetOutline: listFilterWidgetOutline, - listFilterWidgetNoMatchesOutline: listFilterWidgetNoMatchesOutline, + listFilterWidgetBackground, + listFilterWidgetOutline, + listFilterWidgetNoMatchesOutline, listMatchesShadow: widgetShadow, - treeIndentGuidesStroke: treeIndentGuidesStroke + treeIndentGuidesStroke }; export interface IButtonStyleOverrides extends IStyleOverrides { diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index f15dec1b8a7..2d96ce768ad 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -46,7 +46,7 @@ export abstract class AbstractUpdateService implements IUpdateService { constructor( @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @IConfigurationService protected configurationService: IConfigurationService, - @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, + @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @IRequestService protected requestService: IRequestService, @ILogService protected logService: ILogService, ) { } @@ -57,11 +57,11 @@ export abstract class AbstractUpdateService implements IUpdateService { * https://github.com/microsoft/vscode/issues/89784 */ initialize(): void { - if (!this.environmentService.isBuilt) { + if (!this.environmentMainService.isBuilt) { return; // updates are never enabled when running out of sources } - if (this.environmentService.disableUpdates) { + if (this.environmentMainService.disableUpdates) { this.logService.info('update#ctor - updates are disabled by the environment'); return; } diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index ddfa1ee4a5f..8d67a459903 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -32,11 +32,11 @@ export class DarwinUpdateService extends AbstractUpdateService { @ILifecycleMainService lifecycleMainService: ILifecycleMainService, @IConfigurationService configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IEnvironmentMainService environmentService: IEnvironmentMainService, + @IEnvironmentMainService environmentMainService: IEnvironmentMainService, @IRequestService requestService: IRequestService, @ILogService logService: ILogService ) { - super(lifecycleMainService, configurationService, environmentService, requestService, logService); + super(lifecycleMainService, configurationService, environmentMainService, requestService, logService); } initialize(): void { diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts index 91565b13954..d00055b590f 100644 --- a/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -23,12 +23,12 @@ export class LinuxUpdateService extends AbstractUpdateService { @ILifecycleMainService lifecycleMainService: ILifecycleMainService, @IConfigurationService configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IEnvironmentMainService environmentService: IEnvironmentMainService, + @IEnvironmentMainService environmentMainService: IEnvironmentMainService, @IRequestService requestService: IRequestService, @ILogService logService: ILogService, @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService ) { - super(lifecycleMainService, configurationService, environmentService, requestService, logService); + super(lifecycleMainService, configurationService, environmentMainService, requestService, logService); } protected buildUpdateFeedUrl(quality: string): string { diff --git a/src/vs/platform/update/electron-main/updateService.snap.ts b/src/vs/platform/update/electron-main/updateService.snap.ts index 1712182c365..e0d6010d06a 100644 --- a/src/vs/platform/update/electron-main/updateService.snap.ts +++ b/src/vs/platform/update/electron-main/updateService.snap.ts @@ -36,10 +36,10 @@ abstract class AbstractUpdateService2 implements IUpdateService { constructor( @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, - @IEnvironmentMainService environmentService: IEnvironmentMainService, + @IEnvironmentMainService environmentMainService: IEnvironmentMainService, @ILogService protected logService: ILogService, ) { - if (environmentService.disableUpdates) { + if (environmentMainService.disableUpdates) { this.logService.info('update#ctor - updates are disabled'); return; } @@ -140,11 +140,11 @@ export class SnapUpdateService extends AbstractUpdateService2 { private snap: string, private snapRevision: string, @ILifecycleMainService lifecycleMainService: ILifecycleMainService, - @IEnvironmentMainService environmentService: IEnvironmentMainService, + @IEnvironmentMainService environmentMainService: IEnvironmentMainService, @ILogService logService: ILogService, @ITelemetryService private readonly telemetryService: ITelemetryService ) { - super(lifecycleMainService, environmentService, logService); + super(lifecycleMainService, environmentMainService, logService); const watcher = watch(path.dirname(this.snap)); const onChange = Event.fromNodeEventEmitter(watcher, 'change', (_, fileName: string) => fileName); diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index c92a0931dcc..3fcc37e91d4 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -63,13 +63,13 @@ export class Win32UpdateService extends AbstractUpdateService { @ILifecycleMainService lifecycleMainService: ILifecycleMainService, @IConfigurationService configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IEnvironmentMainService environmentService: IEnvironmentMainService, + @IEnvironmentMainService environmentMainService: IEnvironmentMainService, @IRequestService requestService: IRequestService, @ILogService logService: ILogService, @IFileService private readonly fileService: IFileService, @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService ) { - super(lifecycleMainService, configurationService, environmentService, requestService, logService); + super(lifecycleMainService, configurationService, environmentMainService, requestService, logService); } initialize(): void { diff --git a/src/vs/platform/url/electron-main/electronUrlListener.ts b/src/vs/platform/url/electron-main/electronUrlListener.ts index 50ca2ea249c..e281cff7d5e 100644 --- a/src/vs/platform/url/electron-main/electronUrlListener.ts +++ b/src/vs/platform/url/electron-main/electronUrlListener.ts @@ -43,7 +43,7 @@ export class ElectronURLListener { initialUrisToHandle: { uri: URI, url: string }[], private readonly urlService: IURLService, windowsMainService: IWindowsMainService, - environmentService: IEnvironmentMainService + environmentMainService: IEnvironmentMainService ) { // the initial set of URIs we need to handle once the window is ready @@ -51,7 +51,7 @@ export class ElectronURLListener { // Windows: install as protocol handler if (isWindows) { - const windowsParameters = environmentService.isBuilt ? [] : [`"${environmentService.appRoot}"`]; + const windowsParameters = environmentMainService.isBuilt ? [] : [`"${environmentMainService.appRoot}"`]; windowsParameters.push('--open-url', '--'); app.setAsDefaultProtocolClient(product.urlProtocol, process.execPath, windowsParameters); } @@ -82,7 +82,7 @@ export class ElectronURLListener { if (isWindowReady) { this.flush(); } else { - Event.once(windowsMainService.onWindowReady)(this.flush, this, this.disposables); + Event.once(windowsMainService.onDidSignalReadyWindow)(this.flush, this, this.disposables); } } diff --git a/src/vs/platform/webview/electron-main/webviewMainService.ts b/src/vs/platform/webview/electron-main/webviewMainService.ts index f569607a424..dd3984d4ba5 100644 --- a/src/vs/platform/webview/electron-main/webviewMainService.ts +++ b/src/vs/platform/webview/electron-main/webviewMainService.ts @@ -87,7 +87,7 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer if (typeof (id as WebviewWindowId).windowId === 'number') { const { windowId } = (id as WebviewWindowId); const window = this.windowsMainService.getWindowById(windowId); - if (!window) { + if (!window?.win) { throw new Error(`Invalid windowId: ${windowId}`); } contents = window.win.webContents; diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 2ae70d36bce..3ca7fa2b745 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -114,6 +114,7 @@ export interface IWindowSettings { readonly enableMenuBarMnemonics: boolean; readonly closeWhenEmpty: boolean; readonly clickThroughInactive: boolean; + readonly enableExperimentalMainProcessWorkspaceStorage: boolean; } export function getTitleBarStyle(configurationService: IConfigurationService): 'native' | 'custom' { @@ -253,6 +254,8 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native filesToWait?: IPathsToWaitFor; os: IOSConfiguration; + + enableExperimentalMainProcessWorkspaceStorage: boolean; } /** diff --git a/src/vs/platform/windows/electron-main/window.ts b/src/vs/platform/windows/electron-main/window.ts index 7fc86f531bd..a78accbef8e 100644 --- a/src/vs/platform/windows/electron-main/window.ts +++ b/src/vs/platform/windows/electron-main/window.ts @@ -19,7 +19,7 @@ import product from 'vs/platform/product/common/product'; import { WindowMinimumSize, IWindowSettings, MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility, zoomLevelToZoomFactor, INativeWindowConfiguration } from 'vs/platform/windows/common/windows'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { browserCodeLoadingCacheStrategy, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; -import { defaultWindowState, ICodeWindow, IWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows'; +import { defaultWindowState, ICodeWindow, ILoadEvent, IWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows'; import { ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; @@ -32,7 +32,7 @@ import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMain import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; -import { IStorageMainService } from 'vs/platform/storage/node/storageMainService'; +import { IStorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; import { ByteSize, IFileService } from 'vs/platform/files/common/files'; import { FileAccess, Schemas } from 'vs/base/common/network'; import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper'; @@ -80,17 +80,17 @@ export class CodeWindow extends Disposable implements ICodeWindow { private static readonly MAX_URL_LENGTH = 2 * ByteSize.MB; // https://cs.chromium.org/chromium/src/url/url_constants.cc?l=32 - private readonly _onLoad = this._register(new Emitter()); - readonly onLoad = this._onLoad.event; + private readonly _onWillLoad = this._register(new Emitter()); + readonly onWillLoad = this._onWillLoad.event; - private readonly _onReady = this._register(new Emitter()); - readonly onReady = this._onReady.event; + private readonly _onDidSignalReady = this._register(new Emitter()); + readonly onDidSignalReady = this._onDidSignalReady.event; - private readonly _onClose = this._register(new Emitter()); - readonly onClose = this._onClose.event; + private readonly _onDidClose = this._register(new Emitter()); + readonly onDidClose = this._onDidClose.event; - private readonly _onDestroy = this._register(new Emitter()); - readonly onDestroy = this._onDestroy.event; + private readonly _onDidDestroy = this._register(new Emitter()); + readonly onDidDestroy = this._onDidDestroy.event; private hiddenTitleBarStyle: boolean | undefined; private showTimeoutHandle: NodeJS.Timeout | undefined; @@ -114,9 +114,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { constructor( config: IWindowCreationOptions, @ILogService private readonly logService: ILogService, - @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, + @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @IFileService private readonly fileService: IFileService, - @IStorageMainService storageService: IStorageMainService, + @IStorageMainService storageMainService: IStorageMainService, @IConfigurationService private readonly configurationService: IConfigurationService, @IThemeMainService private readonly themeMainService: IThemeMainService, @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @@ -158,7 +158,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { nativeWindowOpen: true, webviewTag: true, zoomFactor: zoomLevelToZoomFactor(windowConfig?.zoomLevel), - ...this.environmentService.sandbox ? + ...this.environmentMainService.sandbox ? // Sandbox { @@ -181,9 +181,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Linux: always // Windows: only when running out of sources, otherwise an icon is set by us on the executable if (isLinux) { - options.icon = join(this.environmentService.appRoot, 'resources/linux/code.png'); - } else if (isWindows && !this.environmentService.isBuilt) { - options.icon = join(this.environmentService.appRoot, 'resources/win32/code_150x150.png'); + options.icon = join(this.environmentMainService.appRoot, 'resources/linux/code.png'); + } else if (isWindows && !this.environmentMainService.isBuilt) { + options.icon = join(this.environmentMainService.appRoot, 'resources/win32/code_150x150.png'); } if (isMacintosh && !this.useNativeFullScreen()) { @@ -217,7 +217,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { this._id = this._win.id; // Open devtools if instructed from command line args - if (this.environmentService.args['open-devtools'] === true) { + if (this.environmentMainService.args['open-devtools'] === true) { this._win.webContents.openDevTools(); } @@ -267,9 +267,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.createTouchBar(); // Request handling - this.marketplaceHeadersPromise = resolveMarketplaceHeaders(product.version, this.environmentService, this.fileService, { - get(key) { return storageService.get(key); }, - store(key, value) { storageService.store(key, value); } + this.marketplaceHeadersPromise = resolveMarketplaceHeaders(product.version, this.environmentMainService, this.fileService, { + get: key => storageMainService.globalStorage.get(key), + store: (key, value) => storageMainService.globalStorage.store(key, value) }); // Eventing @@ -285,7 +285,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { get id(): number { return this._id; } private _win: BrowserWindow; - get win(): BrowserWindow { return this._win; } + get win(): BrowserWindow | null { return this._win; } get hasHiddenTitleBarStyle(): boolean { return !!this.hiddenTitleBarStyle; } @@ -297,7 +297,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { setRepresentedFilename(filename: string): void { if (isMacintosh) { - this.win.setRepresentedFilename(filename); + this._win.setRepresentedFilename(filename); } else { this.representedFilename = filename; } @@ -305,7 +305,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { getRepresentedFilename(): string | undefined { if (isMacintosh) { - return this.win.getRepresentedFilename(); + return this._win.getRepresentedFilename(); } return this.representedFilename; @@ -367,7 +367,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } // Events - this._onReady.fire(); + this._onDidSignalReady.fire(); } ready(): Promise { @@ -395,8 +395,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { resolve(); } - const closeListener = this.onClose(() => handle()); - const loadListener = this.onLoad(() => handle()); + const closeListener = this.onDidClose(() => handle()); + const loadListener = this.onWillLoad(() => handle()); }); } @@ -409,7 +409,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Window close this._win.on('closed', () => { - this._onClose.fire(); + this._onDidClose.fire(); this.dispose(); }); @@ -535,20 +535,23 @@ export class CodeWindow extends Disposable implements ICodeWindow { }); // Handle configuration changes - this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated())); + this._register(this.configurationService.onDidChangeConfiguration(() => this.onConfigurationUpdated())); // Handle Workspace events - this._register(this.workspacesManagementMainService.onUntitledWorkspaceDeleted(e => this.onUntitledWorkspaceDeleted(e))); + this._register(this.workspacesManagementMainService.onDidDeleteUntitledWorkspace(e => this.onDidDeleteUntitledWorkspace(e))); // Inject headers when requests are incoming const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*']; - this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, (details, cb) => - this.marketplaceHeadersPromise.then(headers => cb({ cancel: false, requestHeaders: Object.assign(details.requestHeaders, headers) }))); + this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, async (details, cb) => { + const headers = await this.marketplaceHeadersPromise; + + cb({ cancel: false, requestHeaders: Object.assign(details.requestHeaders, headers) }); + }); } - private onWindowError(error: WindowError.UNRESPONSIVE): void; - private onWindowError(error: WindowError.CRASHED, details: RenderProcessGoneDetails): void; - private onWindowError(error: WindowError, details?: RenderProcessGoneDetails): void { + private async onWindowError(error: WindowError.UNRESPONSIVE): Promise; + private async onWindowError(error: WindowError.CRASHED, details: RenderProcessGoneDetails): Promise; + private async onWindowError(error: WindowError, details?: RenderProcessGoneDetails): Promise { this.logService.error(error === WindowError.CRASHED ? `Main: renderer process crashed (detail: ${details?.reason})` : 'Main: detected unresponsive'); // If we run extension tests from CLI, showing a dialog is not @@ -581,25 +584,25 @@ export class CodeWindow extends Disposable implements ICodeWindow { } // Show Dialog - this.dialogMainService.showMessageBox({ + const result = await this.dialogMainService.showMessageBox({ title: product.nameLong, type: 'warning', buttons: [mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(localize({ key: 'wait', comment: ['&& denotes a mnemonic'] }, "&&Keep Waiting")), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], message: localize('appStalled', "The window is no longer responding"), detail: localize('appStalledDetail', "You can reopen or close the window or keep waiting."), noLink: true - }, this._win).then(result => { - if (!this._win) { - return; // Return early if the window has been going down already - } + }, this._win); - if (result.response === 0) { - this._win.webContents.forcefullyCrashRenderer(); // Calling reload() immediately after calling this method will force the reload to occur in a new process - this.reload(); - } else if (result.response === 2) { - this.destroyWindow(); - } - }); + if (!this._win) { + return; // Return early if the window has been going down already + } + + if (result.response === 0) { + this._win.webContents.forcefullyCrashRenderer(); // Calling reload() immediately after calling this method will force the reload to occur in a new process + this.reload(); + } else if (result.response === 2) { + this.destroyWindow(); + } } // Crashed @@ -611,33 +614,33 @@ export class CodeWindow extends Disposable implements ICodeWindow { message = localize('appCrashed', "The window has crashed", details?.reason); } - this.dialogMainService.showMessageBox({ + const result = await this.dialogMainService.showMessageBox({ title: product.nameLong, type: 'warning', buttons: [mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], message, detail: localize('appCrashedDetail', "We are sorry for the inconvenience! You can reopen the window to continue where you left off."), noLink: true - }, this._win).then(result => { - if (!this._win) { - return; // Return early if the window has been going down already - } + }, this._win); - if (result.response === 0) { - this.reload(); - } else if (result.response === 1) { - this.destroyWindow(); - } - }); + if (!this._win) { + return; // Return early if the window has been going down already + } + + if (result.response === 0) { + this.reload(); + } else if (result.response === 1) { + this.destroyWindow(); + } } } private destroyWindow(): void { - this._onDestroy.fire(); // 'close' event will not be fired on destroy(), so signal crash via explicit event + this._onDidDestroy.fire(); // 'close' event will not be fired on destroy(), so signal crash via explicit event this._win.destroy(); // make sure to destroy the window as it has crashed } - private onUntitledWorkspaceDeleted(workspace: IWorkspaceIdentifier): void { + private onDidDeleteUntitledWorkspace(workspace: IWorkspaceIdentifier): void { // Make sure to update our workspace config if we detect that it // was deleted @@ -677,7 +680,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } addTabbedWindow(window: ICodeWindow): void { - if (isMacintosh) { + if (isMacintosh && window.win) { this._win.addTabbedWindow(window.win); } } @@ -747,7 +750,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Make window visible if it did not open in N seconds because this indicates an error // Only do this when running out of sources and not when running tests - if (!this.environmentService.isBuilt && !this.environmentService.extensionTestsLocationURI) { + if (!this.environmentMainService.isBuilt && !this.environmentMainService.extensionTestsLocationURI) { this.showTimeoutHandle = setTimeout(() => { if (this._win && !this._win.isVisible() && !this._win.isMinimized()) { this._win.show(); @@ -758,7 +761,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } // Event - this._onLoad.fire(); + this._onWillLoad.fire({ workspace: configuration.workspace }); } async reload(cli?: NativeParsedArgs): Promise { @@ -824,7 +827,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { windowConfiguration.windowId = this._win.id; windowConfiguration.sessionId = `window:${this._win.id}`; windowConfiguration.logLevel = this.logService.getLevel(); - windowConfiguration.logsPath = this.environmentService.logsPath; + windowConfiguration.logsPath = this.environmentMainService.logsPath; // Set zoomlevel const windowConfig = this.configurationService.getValue('window'); @@ -851,13 +854,15 @@ export class CodeWindow extends Disposable implements ICodeWindow { windowConfiguration.perfMarks = getMarks(); // Parts splash - windowConfiguration.partsSplashPath = join(this.environmentService.userDataPath, 'rapid_render.json'); + windowConfiguration.partsSplashPath = join(this.environmentMainService.userDataPath, 'rapid_render.json'); // OS Info windowConfiguration.os = { release: release() }; + windowConfiguration.enableExperimentalMainProcessWorkspaceStorage = !!(windowConfig?.enableExperimentalMainProcessWorkspaceStorage); + // Config (combination of process.argv and window configuration) const environment = parseArgs(process.argv, OPTIONS); const config = Object.assign(environment, windowConfiguration) as unknown as { [key: string]: unknown }; @@ -887,7 +892,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { private doGetUrl(config: object): string { let workbench: string; - if (this.environmentService.sandbox) { + if (this.environmentMainService.sandbox) { workbench = 'vs/code/electron-sandbox/workbench/workbench.html'; } else { workbench = 'vs/code/electron-browser/workbench/workbench.html'; @@ -1257,26 +1262,26 @@ export class CodeWindow extends Disposable implements ICodeWindow { const action = systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string'); switch (action) { case 'Minimize': - this.win.minimize(); + this._win.minimize(); break; case 'None': break; case 'Maximize': default: - if (this.win.isMaximized()) { - this.win.unmaximize(); + if (this._win.isMaximized()) { + this._win.unmaximize(); } else { - this.win.maximize(); + this._win.maximize(); } } } // Linux/Windows: just toggle maximize/minimized state else { - if (this.win.isMaximized()) { - this.win.unmaximize(); + if (this._win.isMaximized()) { + this._win.unmaximize(); } else { - this.win.maximize(); + this._win.maximize(); } } } diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index c309ff263f4..5a1446b4c4f 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -60,17 +60,21 @@ export const enum WindowMode { Fullscreen } +export interface ILoadEvent { + workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined; +} + export interface ICodeWindow extends IDisposable { - readonly onLoad: Event; - readonly onReady: Event; - readonly onClose: Event; - readonly onDestroy: Event; + readonly onWillLoad: Event; + readonly onDidSignalReady: Event; + readonly onDidClose: Event; + readonly onDidDestroy: Event; readonly whenClosedOrLoaded: Promise; readonly id: number; - readonly win: BrowserWindow; + readonly win: BrowserWindow | null; /* `null` after being disposed */ readonly config: INativeWindowConfiguration | undefined; readonly openedWorkspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; @@ -132,11 +136,11 @@ export interface IWindowsMainService { readonly _serviceBrand: undefined; - readonly onWindowsCountChanged: Event; + readonly onDidChangeWindowsCount: Event; - readonly onWindowOpened: Event; - readonly onWindowReady: Event; - readonly onWindowDestroyed: Event; + readonly onDidOpenWindow: Event; + readonly onDidSignalReadyWindow: Event; + readonly onDidDestroyWindow: Event; open(openConfig: IOpenConfiguration): ICodeWindow[]; openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): ICodeWindow[]; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index c7625528cf4..f68cf41be11 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -14,7 +14,7 @@ import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { IStateService } from 'vs/platform/state/node/state'; import { CodeWindow } from 'vs/platform/windows/electron-main/window'; import { BrowserWindow, MessageBoxOptions, WebContents } from 'electron'; -import { ILifecycleMainService, UnloadReason, LifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; +import { ILifecycleMainService, UnloadReason, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { IWindowSettings, IPath, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions, IAddFoldersRequest, IPathsToWaitFor, INativeWindowConfiguration, INativeOpenFileRequest } from 'vs/platform/windows/common/windows'; @@ -35,7 +35,7 @@ import { getSingleFolderWorkspaceIdentifier, getWorkspaceIdentifier, IWorkspaces import { once } from 'vs/base/common/functional'; import { Disposable } from 'vs/base/common/lifecycle'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; -import { withNullAsUndefined } from 'vs/base/common/types'; +import { assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; import { isWindowsDriveLetter, toSlashes, parseLineAndColumnAware, sanitizeFilePath } from 'vs/base/common/extpath'; import { CharCode } from 'vs/base/common/charCode'; import { getPathLabel } from 'vs/base/common/labels'; @@ -118,17 +118,17 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic private static readonly WINDOWS: ICodeWindow[] = []; - private readonly _onWindowOpened = this._register(new Emitter()); - readonly onWindowOpened = this._onWindowOpened.event; + private readonly _onDidOpenWindow = this._register(new Emitter()); + readonly onDidOpenWindow = this._onDidOpenWindow.event; - private readonly _onWindowReady = this._register(new Emitter()); - readonly onWindowReady = this._onWindowReady.event; + private readonly _onDidSignalReadyWindow = this._register(new Emitter()); + readonly onDidSignalReadyWindow = this._onDidSignalReadyWindow.event; - private readonly _onWindowDestroyed = this._register(new Emitter()); - readonly onWindowDestroyed = this._onWindowDestroyed.event; + private readonly _onDidDestroyWindow = this._register(new Emitter()); + readonly onDidDestroyWindow = this._onDidDestroyWindow.event; - private readonly _onWindowsCountChanged = this._register(new Emitter()); - readonly onWindowsCountChanged = this._onWindowsCountChanged.event; + private readonly _onDidChangeWindowsCount = this._register(new Emitter()); + readonly onDidChangeWindowsCount = this._onDidChangeWindowsCount.event; private readonly windowsStateHandler = this._register(new WindowsStateHandler(this, this.stateService, this.lifecycleMainService, this.logService, this.configurationService)); @@ -137,7 +137,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic private readonly initialUserEnv: IProcessEnvironment, @ILogService private readonly logService: ILogService, @IStateService private readonly stateService: IStateService, - @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, + @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @IBackupMainService private readonly backupMainService: IBackupMainService, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -155,11 +155,11 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic private registerListeners(): void { // Signal a window is ready after having entered a workspace - this._register(this.workspacesManagementMainService.onWorkspaceEntered(event => this._onWindowReady.fire(event.window))); + this._register(this.workspacesManagementMainService.onDidEnterWorkspace(event => this._onDidSignalReadyWindow.fire(event.window))); } openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): ICodeWindow[] { - let cli = this.environmentService.args; + let cli = this.environmentMainService.args; const remote = options?.remoteAuthority; if (cli && (cli.remote !== remote)) { cli = { ...cli, remote }; @@ -670,7 +670,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic buttons: [localize('ok', "OK")], message: uri.scheme === Schemas.file ? localize('pathNotExistTitle', "Path does not exist") : localize('uriInvalidTitle', "URI can not be opened"), detail: uri.scheme === Schemas.file ? - localize('pathNotExistDetail', "The path '{0}' does not seem to exist anymore on disk.", getPathLabel(uri.fsPath, this.environmentService)) : + localize('pathNotExistDetail', "The path '{0}' does not seem to exist anymore on disk.", getPathLabel(uri.fsPath, this.environmentMainService)) : localize('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", uri.toString()), noLink: true }; @@ -1135,9 +1135,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Build `INativeWindowConfiguration` from config and options const configuration = { ...options.cli } as INativeWindowConfiguration; - configuration.appRoot = this.environmentService.appRoot; + configuration.appRoot = this.environmentMainService.appRoot; configuration.machineId = this.machineId; - configuration.nodeCachedDataDir = this.environmentService.nodeCachedDataDir; + configuration.nodeCachedDataDir = this.environmentMainService.nodeCachedDataDir; configuration.mainPid = process.pid; configuration.execPath = process.execPath; configuration.userEnv = { ...this.initialUserEnv, ...options.userEnv }; @@ -1157,7 +1157,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // For all other cases we first call into registerEmptyWindowBackupSync() to set it before // loading the window. if (options.emptyWindowBackupInfo) { - configuration.backupPath = join(this.environmentService.backupHome, options.emptyWindowBackupInfo.backupFolder); + configuration.backupPath = join(this.environmentMainService.backupHome, options.emptyWindowBackupInfo.backupFolder); } let window: ICodeWindow | undefined; @@ -1191,20 +1191,22 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic WindowsMainService.WINDOWS.push(createdWindow); // Indicate new window via event - this._onWindowOpened.fire(createdWindow); + this._onDidOpenWindow.fire(createdWindow); // Indicate number change via event - this._onWindowsCountChanged.fire({ oldCount: this.getWindowCount() - 1, newCount: this.getWindowCount() }); + this._onDidChangeWindowsCount.fire({ oldCount: this.getWindowCount() - 1, newCount: this.getWindowCount() }); // Window Events - once(createdWindow.onReady)(() => this._onWindowReady.fire(createdWindow)); - once(createdWindow.onClose)(() => this.onWindowClosed(createdWindow)); - once(createdWindow.onDestroy)(() => this._onWindowDestroyed.fire(createdWindow)); - createdWindow.win.webContents.removeAllListeners('devtools-reload-page'); // remove built in listener so we can handle this on our own - createdWindow.win.webContents.on('devtools-reload-page', () => this.lifecycleMainService.reload(createdWindow)); + once(createdWindow.onDidSignalReady)(() => this._onDidSignalReadyWindow.fire(createdWindow)); + once(createdWindow.onDidClose)(() => this.onWindowClosed(createdWindow)); + once(createdWindow.onDidDestroy)(() => this._onDidDestroyWindow.fire(createdWindow)); + + const webContents = assertIsDefined(createdWindow.win?.webContents); + webContents.removeAllListeners('devtools-reload-page'); // remove built in listener so we can handle this on our own + webContents.on('devtools-reload-page', () => this.lifecycleMainService.reload(createdWindow)); // Lifecycle - (this.lifecycleMainService as LifecycleMainService).registerWindow(createdWindow); + this.lifecycleMainService.registerWindow(createdWindow); } // Existing window @@ -1264,7 +1266,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic WindowsMainService.WINDOWS.splice(index, 1); // Emit - this._onWindowsCountChanged.fire({ oldCount: this.getWindowCount() + 1, newCount: this.getWindowCount() }); + this._onDidChangeWindowsCount.fire({ oldCount: this.getWindowCount() + 1, newCount: this.getWindowCount() }); } getFocusedWindow(): ICodeWindow | undefined { diff --git a/src/vs/platform/windows/electron-main/windowsStateHandler.ts b/src/vs/platform/windows/electron-main/windowsStateHandler.ts index b0e50c85ff4..75c96494b80 100644 --- a/src/vs/platform/windows/electron-main/windowsStateHandler.ts +++ b/src/vs/platform/windows/electron-main/windowsStateHandler.ts @@ -83,9 +83,9 @@ export class WindowsStateHandler extends Disposable { }); // Handle various lifecycle events around windows - this.lifecycleMainService.onBeforeWindowClose(window => this.onBeforeWindowClose(window)); + this.lifecycleMainService.onBeforeCloseWindow(window => this.onBeforeCloseWindow(window)); this.lifecycleMainService.onBeforeShutdown(() => this.onBeforeShutdown()); - this.windowsMainService.onWindowsCountChanged(e => { + this.windowsMainService.onDidChangeWindowsCount(e => { if (e.newCount - e.oldCount > 0) { // clear last closed window state when a new window opens. this helps on macOS where // otherwise closing the last window, opening a new window and then quitting would @@ -95,16 +95,16 @@ export class WindowsStateHandler extends Disposable { }); // try to save state before destroy because close will not fire - this.windowsMainService.onWindowDestroyed(window => this.onBeforeWindowClose(window)); + this.windowsMainService.onDidDestroyWindow(window => this.onBeforeCloseWindow(window)); } - // Note that onBeforeShutdown() and onBeforeWindowClose() are fired in different order depending on the OS: + // Note that onBeforeShutdown() and onBeforeCloseWindow() are fired in different order depending on the OS: // - macOS: since the app will not quit when closing the last window, you will always first get - // the onBeforeShutdown() event followed by N onBeforeWindowClose() events for each window + // the onBeforeShutdown() event followed by N onBeforeCloseWindow() events for each window // - other: on other OS, closing the last window will quit the app so the order depends on the - // user interaction: closing the last window will first trigger onBeforeWindowClose() + // user interaction: closing the last window will first trigger onBeforeCloseWindow() // and then onBeforeShutdown(). Using the quit action however will first issue onBeforeShutdown() - // and then onBeforeWindowClose(). + // and then onBeforeCloseWindow(). // // Here is the behavior on different OS depending on action taken (Electron 1.7.x): // @@ -113,27 +113,27 @@ export class WindowsStateHandler extends Disposable { // - close(1): close one window via the window close button // - closeAll: close all windows via the taskbar command // - onBeforeShutdown(N): number of windows reported in this event handler - // - onBeforeWindowClose(N, M): number of windows reported and quitRequested boolean in this event handler + // - onBeforeCloseWindow(N, M): number of windows reported and quitRequested boolean in this event handler // // macOS - // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) - // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) + // - quit(1): onBeforeShutdown(1), onBeforeCloseWindow(1, true) + // - quit(2): onBeforeShutdown(2), onBeforeCloseWindow(2, true), onBeforeCloseWindow(2, true) // - quit(0): onBeforeShutdown(0) - // - close(1): onBeforeWindowClose(1, false) + // - close(1): onBeforeCloseWindow(1, false) // // Windows - // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) - // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) - // - close(1): onBeforeWindowClose(2, false)[not last window] - // - close(1): onBeforeWindowClose(1, false), onBeforeShutdown(0)[last window] - // - closeAll(2): onBeforeWindowClose(2, false), onBeforeWindowClose(2, false), onBeforeShutdown(0) + // - quit(1): onBeforeShutdown(1), onBeforeCloseWindow(1, true) + // - quit(2): onBeforeShutdown(2), onBeforeCloseWindow(2, true), onBeforeCloseWindow(2, true) + // - close(1): onBeforeCloseWindow(2, false)[not last window] + // - close(1): onBeforeCloseWindow(1, false), onBeforeShutdown(0)[last window] + // - closeAll(2): onBeforeCloseWindow(2, false), onBeforeCloseWindow(2, false), onBeforeShutdown(0) // // Linux - // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) - // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) - // - close(1): onBeforeWindowClose(2, false)[not last window] - // - close(1): onBeforeWindowClose(1, false), onBeforeShutdown(0)[last window] - // - closeAll(2): onBeforeWindowClose(2, false), onBeforeWindowClose(2, false), onBeforeShutdown(0) + // - quit(1): onBeforeShutdown(1), onBeforeCloseWindow(1, true) + // - quit(2): onBeforeShutdown(2), onBeforeCloseWindow(2, true), onBeforeCloseWindow(2, true) + // - close(1): onBeforeCloseWindow(2, false)[not last window] + // - close(1): onBeforeCloseWindow(1, false), onBeforeShutdown(0)[last window] + // - closeAll(2): onBeforeCloseWindow(2, false), onBeforeCloseWindow(2, false), onBeforeShutdown(0) // private onBeforeShutdown(): void { this.shuttingDown = true; @@ -185,7 +185,7 @@ export class WindowsStateHandler extends Disposable { } // See note on #onBeforeShutdown() for details how these events are flowing - private onBeforeWindowClose(window: ICodeWindow): void { + private onBeforeCloseWindow(window: ICodeWindow): void { if (this.lifecycleMainService.quitRequested) { return; // during quit, many windows close in parallel so let it be handled in the before-quit handler } diff --git a/src/vs/platform/windows/test/electron-main/window.test.ts b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts similarity index 95% rename from src/vs/platform/windows/test/electron-main/window.test.ts rename to src/vs/platform/windows/test/electron-main/windowsFinder.test.ts index 94ae6eea848..1fd596212bd 100644 --- a/src/vs/platform/windows/test/electron-main/window.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { join } from 'vs/base/common/path'; import { findWindowOnFile } from 'vs/platform/windows/electron-main/windowsFinder'; -import { ICodeWindow, IWindowState } from 'vs/platform/windows/electron-main/windows'; +import { ICodeWindow, ILoadEvent, IWindowState } from 'vs/platform/windows/electron-main/windows'; import { IWorkspaceIdentifier, toWorkspaceFolders } from 'vs/platform/workspaces/common/workspaces'; import { URI } from 'vs/base/common/uri'; import { getPathFromAmdModule } from 'vs/base/common/amd'; @@ -32,13 +32,13 @@ suite('WindowsFinder', () => { function createTestCodeWindow(options: { lastFocusTime: number, openedFolderUri?: URI, openedWorkspace?: IWorkspaceIdentifier }): ICodeWindow { return new class implements ICodeWindow { - onLoad: Event = Event.None; - onReady: Event = Event.None; - onClose: Event = Event.None; - onDestroy: Event = Event.None; + onWillLoad: Event = Event.None; + onDidSignalReady: Event = Event.None; + onDidClose: Event = Event.None; + onDidDestroy: Event = Event.None; whenClosedOrLoaded: Promise = Promise.resolve(); id: number = -1; - win: Electron.BrowserWindow = undefined!; + win: Electron.BrowserWindow = null!; config: INativeWindowConfiguration | undefined; openedWorkspace = options.openedFolderUri ? { id: '', uri: options.openedFolderUri } : options.openedWorkspace; backupPath?: string | undefined; diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index 66413f2e4e3..8be3cea72d7 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -45,7 +45,7 @@ export interface IWorkspacesService { getWorkspaceIdentifier(workspacePath: URI): Promise; // Workspaces History - readonly onRecentlyOpenedChange: Event; + readonly onDidChangeRecentlyOpened: Event; addRecentlyOpened(recents: IRecent[]): Promise; removeRecentlyOpened(workspaces: URI[]): Promise; clearRecentlyOpened(): Promise; @@ -116,6 +116,10 @@ export interface ISingleFolderWorkspaceIdentifier extends IBaseWorkspaceIdentifi uri: URI; } +export interface ISerializedSingleFolderWorkspaceIdentifier extends IBaseWorkspaceIdentifier { + uri: UriComponents; +} + export function isSingleFolderWorkspaceIdentifier(obj: unknown): obj is ISingleFolderWorkspaceIdentifier { const singleFolderIdentifier = obj as ISingleFolderWorkspaceIdentifier | undefined; @@ -133,6 +137,10 @@ export interface IWorkspaceIdentifier extends IBaseWorkspaceIdentifier { configPath: URI; } +export interface ISerializedWorkspaceIdentifier extends IBaseWorkspaceIdentifier { + configPath: UriComponents; +} + export function toWorkspaceIdentifier(workspace: IWorkspace): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { // Multi root @@ -161,13 +169,28 @@ export function isWorkspaceIdentifier(obj: unknown): obj is IWorkspaceIdentifier return typeof workspaceIdentifier?.id === 'string' && URI.isUri(workspaceIdentifier.configPath); } -export function reviveIdentifier(identifier: { id: string, uri?: UriComponents, configPath?: UriComponents } | undefined): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { - if (identifier?.uri) { - return { id: identifier.id, uri: URI.revive(identifier.uri) }; +export function reviveIdentifier(identifier: undefined): undefined; +export function reviveIdentifier(identifier: ISerializedWorkspaceIdentifier): IWorkspaceIdentifier; +export function reviveIdentifier(identifier: ISerializedSingleFolderWorkspaceIdentifier): ISingleFolderWorkspaceIdentifier; +export function reviveIdentifier(identifier: IEmptyWorkspaceIdentifier): IEmptyWorkspaceIdentifier; +export function reviveIdentifier(identifier: ISerializedWorkspaceIdentifier | ISerializedSingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined; +export function reviveIdentifier(identifier: ISerializedWorkspaceIdentifier | ISerializedSingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined { + + // Single Folder + const singleFolderIdentifierCandidate = identifier as ISerializedSingleFolderWorkspaceIdentifier | undefined; + if (singleFolderIdentifierCandidate?.uri) { + return { id: singleFolderIdentifierCandidate.id, uri: URI.revive(singleFolderIdentifierCandidate.uri) }; } - if (identifier?.configPath) { - return { id: identifier.id, configPath: URI.revive(identifier.configPath) }; + // Multi folder + const workspaceIdentifierCandidate = identifier as ISerializedWorkspaceIdentifier | undefined; + if (workspaceIdentifierCandidate?.configPath) { + return { id: workspaceIdentifierCandidate.id, configPath: URI.revive(workspaceIdentifierCandidate.configPath) }; + } + + // Empty + if (identifier?.id) { + return { id: identifier.id }; } return undefined; @@ -177,9 +200,9 @@ export function isUntitledWorkspace(path: URI, environmentService: IEnvironmentS return extUriBiasedIgnorePathCase.isEqualOrParent(path, environmentService.untitledWorkspacesHome); } -export interface IEmptyWorkspaceInitializationPayload extends IBaseWorkspaceIdentifier { } +export interface IEmptyWorkspaceIdentifier extends IBaseWorkspaceIdentifier { } -export type IWorkspaceInitializationPayload = IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceInitializationPayload; +export type IWorkspaceInitializationPayload = IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier; //#endregion diff --git a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts index bd16a20bbb8..22cec5f95ad 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts @@ -30,7 +30,7 @@ export interface IWorkspacesHistoryMainService { readonly _serviceBrand: undefined; - readonly onRecentlyOpenedChange: CommonEvent; + readonly onDidChangeRecentlyOpened: CommonEvent; addRecentlyOpened(recents: IRecent[]): void; getRecentlyOpened(include?: ICodeWindow): IRecentlyOpened; @@ -57,8 +57,8 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa declare readonly _serviceBrand: undefined; - private readonly _onRecentlyOpenedChange = this._register(new Emitter()); - readonly onRecentlyOpenedChange: CommonEvent = this._onRecentlyOpenedChange.event; + private readonly _onDidChangeRecentlyOpened = this._register(new Emitter()); + readonly onDidChangeRecentlyOpened: CommonEvent = this._onDidChangeRecentlyOpened.event; private readonly macOSRecentDocumentsUpdater = this._register(new ThrottledDelayer(800)); @@ -66,7 +66,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa @IStateService private readonly stateService: IStateService, @ILogService private readonly logService: ILogService, @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, - @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, + @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService ) { super(); @@ -80,7 +80,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this.handleWindowsJumpList()); // Add to history when entering workspace - this._register(this.workspacesManagementMainService.onWorkspaceEntered(event => this.addRecentlyOpened([{ workspace: event.workspace }]))); + this._register(this.workspacesManagementMainService.onDidEnterWorkspace(event => this.addRecentlyOpened([{ workspace: event.workspace }]))); } private handleWindowsJumpList(): void { @@ -89,7 +89,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa } this.updateWindowsJumpList(); - this._register(this.onRecentlyOpenedChange(() => this.updateWindowsJumpList())); + this._register(this.onDidChangeRecentlyOpened(() => this.updateWindowsJumpList())); } addRecentlyOpened(recentToAdd: IRecent[]): void { @@ -139,7 +139,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa } this.saveRecentlyOpened({ workspaces, files }); - this._onRecentlyOpenedChange.fire(); + this._onDidChangeRecentlyOpened.fire(); // Schedule update to recent documents on macOS dock if (isMacintosh) { @@ -165,7 +165,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa if (workspaces.length !== mru.workspaces.length || files.length !== mru.files.length) { this.saveRecentlyOpened({ files, workspaces }); - this._onRecentlyOpenedChange.fire(); + this._onDidChangeRecentlyOpened.fire(); // Schedule update to recent documents on macOS dock if (isMacintosh) { @@ -238,7 +238,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa app.clearRecentDocuments(); // Event - this._onRecentlyOpenedChange.fire(); + this._onDidChangeRecentlyOpened.fire(); } getRecentlyOpened(include?: ICodeWindow): IRecentlyOpened { @@ -402,7 +402,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa } // Workspace: Untitled - if (extUriBiasedIgnorePathCase.isEqualOrParent(workspace.configPath, this.environmentService.userHome)) { + if (extUriBiasedIgnorePathCase.isEqualOrParent(workspace.configPath, this.environmentMainService.userHome)) { return { title: localize('untitledWorkspace', "Untitled (Workspace)"), description: '' }; } diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index 5328a07dd5e..67a5e54db3f 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -50,7 +50,7 @@ export class WorkspacesMainService implements AddFirstParameterToFunctions { return this.workspacesHistoryMainService.getRecentlyOpened(this.windowsMainService.getWindowById(windowId)); diff --git a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts index c9d22ba0af3..cb397ad60e8 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts @@ -38,8 +38,8 @@ export interface IWorkspacesManagementMainService { readonly _serviceBrand: undefined; - readonly onUntitledWorkspaceDeleted: Event; - readonly onWorkspaceEntered: Event; + readonly onDidDeleteUntitledWorkspace: Event; + readonly onDidEnterWorkspace: Event; enterWorkspace(intoWindow: ICodeWindow, openedWindows: ICodeWindow[], path: URI): Promise; @@ -65,16 +65,16 @@ export class WorkspacesManagementMainService extends Disposable implements IWork declare readonly _serviceBrand: undefined; - private readonly untitledWorkspacesHome = this.environmentService.untitledWorkspacesHome; // local URI that contains all untitled workspaces + private readonly untitledWorkspacesHome = this.environmentMainService.untitledWorkspacesHome; // local URI that contains all untitled workspaces - private readonly _onUntitledWorkspaceDeleted = this._register(new Emitter()); - readonly onUntitledWorkspaceDeleted: Event = this._onUntitledWorkspaceDeleted.event; + private readonly _onDidDeleteUntitledWorkspace = this._register(new Emitter()); + readonly onDidDeleteUntitledWorkspace: Event = this._onDidDeleteUntitledWorkspace.event; - private readonly _onWorkspaceEntered = this._register(new Emitter()); - readonly onWorkspaceEntered: Event = this._onWorkspaceEntered.event; + private readonly _onDidEnterWorkspace = this._register(new Emitter()); + readonly onDidEnterWorkspace: Event = this._onDidEnterWorkspace.event; constructor( - @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, + @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ILogService private readonly logService: ILogService, @IBackupMainService private readonly backupMainService: IBackupMainService, @IDialogMainService private readonly dialogMainService: IDialogMainService @@ -101,7 +101,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork } private isWorkspacePath(uri: URI): boolean { - return isUntitledWorkspace(uri, this.environmentService) || hasWorkspaceFileExtension(uri); + return isUntitledWorkspace(uri, this.environmentMainService) || hasWorkspaceFileExtension(uri); } private doResolveWorkspace(path: URI, contents: string): IResolvedWorkspace | null { @@ -186,7 +186,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork } isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean { - return isUntitledWorkspace(workspace.configPath, this.environmentService); + return isUntitledWorkspace(workspace.configPath, this.environmentMainService); } deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void { @@ -198,7 +198,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork this.doDeleteUntitledWorkspaceSync(workspace); // Event - this._onUntitledWorkspaceDeleted.fire(workspace); + this._onDidDeleteUntitledWorkspace.fire(workspace); } async deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise { @@ -213,7 +213,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork rimrafSync(dirname(configPath)); // Mark Workspace Storage to be deleted - const workspaceStoragePath = join(this.environmentService.workspaceStorageHome.fsPath, workspace.id); + const workspaceStoragePath = join(this.environmentMainService.workspaceStorageHome.fsPath, workspace.id); if (existsSync(workspaceStoragePath)) { writeFileSync(join(workspaceStoragePath, 'obsolete'), ''); } @@ -260,7 +260,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork } // Emit as event - this._onWorkspaceEntered.fire({ window, workspace: result.workspace }); + this._onDidEnterWorkspace.fire({ window, workspace: result.workspace }); return result; } diff --git a/src/vs/platform/workspaces/test/common/workspaces.test.ts b/src/vs/platform/workspaces/test/common/workspaces.test.ts index 270eaee4899..cb8c60e8668 100644 --- a/src/vs/platform/workspaces/test/common/workspaces.test.ts +++ b/src/vs/platform/workspaces/test/common/workspaces.test.ts @@ -5,10 +5,25 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; -import { hasWorkspaceFileExtension, toWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { hasWorkspaceFileExtension, toWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, ISerializedWorkspaceIdentifier, reviveIdentifier, ISerializedSingleFolderWorkspaceIdentifier, IEmptyWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; suite('Workspaces', () => { + test('reviveIdentifier', () => { + let serializedWorkspaceIdentifier: ISerializedWorkspaceIdentifier = { id: 'id', configPath: URI.file('foo').toJSON() }; + assert.strictEqual(isWorkspaceIdentifier(reviveIdentifier(serializedWorkspaceIdentifier)), true); + + let serializedSingleFolderWorkspaceIdentifier: ISerializedSingleFolderWorkspaceIdentifier = { id: 'id', uri: URI.file('foo').toJSON() }; + assert.strictEqual(isSingleFolderWorkspaceIdentifier(reviveIdentifier(serializedSingleFolderWorkspaceIdentifier)), true); + + let serializedEmptyWorkspaceIdentifier: IEmptyWorkspaceIdentifier = { id: 'id' }; + assert.strictEqual(reviveIdentifier(serializedEmptyWorkspaceIdentifier).id, serializedEmptyWorkspaceIdentifier.id); + assert.strictEqual(isWorkspaceIdentifier(serializedEmptyWorkspaceIdentifier), false); + assert.strictEqual(isSingleFolderWorkspaceIdentifier(serializedEmptyWorkspaceIdentifier), false); + + assert.strictEqual(reviveIdentifier(undefined), undefined); + }); + test('hasWorkspaceFileExtension', () => { assert.strictEqual(hasWorkspaceFileExtension('something'), false); assert.strictEqual(hasWorkspaceFileExtension('something.code-workspace'), true); diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts index fc4b803c38a..55ad732b8b3 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts @@ -81,14 +81,14 @@ suite('WorkspacesManagementMainService', () => { let testDir: string; let untitledWorkspacesHomePath: string; - let environmentService: EnvironmentMainService; + let environmentMainService: EnvironmentMainService; let service: WorkspacesManagementMainService; setup(async () => { testDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'workspacesmanagementmainservice'); untitledWorkspacesHomePath = path.join(testDir, 'Workspaces'); - environmentService = new class TestEnvironmentService extends EnvironmentMainService { + environmentMainService = new class TestEnvironmentService extends EnvironmentMainService { constructor() { super(parseArgs(process.argv, OPTIONS)); } @@ -97,7 +97,7 @@ suite('WorkspacesManagementMainService', () => { } }; - service = new WorkspacesManagementMainService(environmentService, new NullLogService(), new TestBackupMainService(), new TestDialogMainService()); + service = new WorkspacesManagementMainService(environmentMainService, new NullLogService(), new TestBackupMainService(), new TestDialogMainService()); return fs.promises.mkdir(untitledWorkspacesHomePath, { recursive: true }); }); diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index febf42232e6..6bd29a1c0f9 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -64,29 +64,25 @@ declare module 'vscode' { readonly onDidChangeSessions: Event; /** - * Returns an array of current sessions. - * - * TODO @RMacfarlane finish deprecating this and remove it + * Get a list of sessions. + * @param scopes An optional list of scopes. If provided, the sessions returned should match + * these permissions, otherwise all sessions should be returned. + * @returns A promise that resolves to an array of authentication sessions. */ // eslint-disable-next-line vscode-dts-provider-naming - getAllSessions(): Thenable>; - - - /** - * Returns an array of current sessions. - */ - // eslint-disable-next-line vscode-dts-provider-naming - getSessions(scopes: string[]): Thenable>; + getSessions(scopes?: string[]): Thenable>; /** * Prompts a user to login. + * @param scopes A list of scopes, permissions, that the new session should be created with. + * @returns A promise that resolves to an authentication session. */ // eslint-disable-next-line vscode-dts-provider-naming createSession(scopes: string[]): Thenable; /** * Removes the session corresponding to session id. - * @param sessionId The session id to log out of + * @param sessionId The id of the session to remove. */ // eslint-disable-next-line vscode-dts-provider-naming removeSession(sessionId: string): Thenable; @@ -1108,7 +1104,9 @@ declare module 'vscode' { export interface NotebookDocument { readonly uri: Uri; readonly version: number; + // todo@API don't have this... readonly fileName: string; + // todo@API should we really expose this? readonly viewType: string; readonly isDirty: boolean; readonly isUntitled: boolean; @@ -1118,13 +1116,14 @@ declare module 'vscode' { } // todo@API maybe have a NotebookCellPosition sibling - // todo@API should be a class - export interface NotebookCellRange { + export class NotebookCellRange { readonly start: number; /** * exclusive */ readonly end: number; + + constructor(start: number, end: number); } export enum NotebookEditorRevealType { @@ -1309,6 +1308,8 @@ declare module 'vscode' { export namespace notebook { + // todo@API should we really support to pass the viewType? We do NOT support + // to open the same file with different viewTypes at the same time export function openNotebookDocument(uri: Uri, viewType?: string): Thenable; export const onDidOpenNotebookDocument: Event; export const onDidCloseNotebookDocument: Event; @@ -1323,6 +1324,9 @@ declare module 'vscode' { export const onDidChangeNotebookDocumentMetadata: Event; export const onDidChangeNotebookCells: Event; export const onDidChangeCellOutputs: Event; + + // todo@API we send document close and open events when the language of a document changes and + // I believe we should stick that for cells as well export const onDidChangeCellLanguage: Event; export const onDidChangeCellMetadata: Event; } @@ -1334,6 +1338,7 @@ declare module 'vscode' { export const onDidChangeActiveNotebookEditor: Event; export const onDidChangeNotebookEditorSelection: Event; export const onDidChangeNotebookEditorVisibleRanges: Event; + // TODO@API add overload for just a URI export function showNotebookDocument(document: NotebookDocument, options?: NotebookDocumentShowOptions): Thenable; } diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index dfad0388642..64f7d14880f 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -89,14 +89,10 @@ export class MainThreadAuthenticationProvider extends Disposable { } } - async getSessions(scopes: string[]) { + async getSessions(scopes?: string[]) { return this._proxy.$getSessions(this.id, scopes); } - async getAllSessions(): Promise> { - return this._proxy.$getAllSessions(this.id); - } - createSession(scopes: string[]): Promise { return this._proxy.$createSession(this.id, scopes); } diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 617f46a2a2c..5866a81d96a 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -10,7 +10,8 @@ import { Emitter } from 'vs/base/common/event'; import { IRelativePattern } from 'vs/base/common/glob'; import { combinedDisposable, Disposable, DisposableStore, dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; -import { IExtUri } from 'vs/base/common/resources'; +import { Schemas } from 'vs/base/common/network'; +import { IExtUri, isEqual } from 'vs/base/common/resources'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -30,6 +31,7 @@ import { IEditorGroup, IEditorGroupsService, preferredSideBySideGroupDirection } import { openEditorWith } from 'vs/workbench/services/editor/common/editorOpenWith'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ExtHostContext, ExtHostNotebookShape, IExtHostContext, INotebookCellStatusBarEntryDto, INotebookDocumentsAndEditorsDelta, INotebookDocumentShowOptions, INotebookModelAddedData, MainContext, MainThreadNotebookShape, NotebookEditorRevealType, NotebookExtensionDescription } from '../common/extHost.protocol'; class DocumentAndEditorState { @@ -112,23 +114,23 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo private readonly _notebookProviders = new Map(); private readonly _notebookKernelProviders = new Map, provider: IDisposable }>(); private readonly _proxy: ExtHostNotebookShape; - private _toDisposeOnEditorRemove = new Map(); + private readonly _toDisposeOnEditorRemove = new Map(); private _currentState?: DocumentAndEditorState; - private _editorEventListenersMapping: Map = new Map(); - private _documentEventListenersMapping: ResourceMap = new ResourceMap(); + private readonly _editorEventListenersMapping: Map = new Map(); + private readonly _documentEventListenersMapping: ResourceMap = new ResourceMap(); private readonly _cellStatusBarEntries: Map = new Map(); private readonly _modelReferenceCollection: BoundModelReferenceCollection; constructor( extHostContext: IExtHostContext, + @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, @INotebookService private _notebookService: INotebookService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IEditorService private readonly editorService: IEditorService, - @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, - @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService, - @ILogService private readonly logService: ILogService, - @INotebookCellStatusBarService private readonly cellStatusBarService: INotebookCellStatusBarService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IEditorService private readonly _editorService: IEditorService, + @IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService, + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, + @ILogService private readonly _logService: ILogService, + @INotebookCellStatusBarService private readonly _cellStatusBarService: INotebookCellStatusBarService, @INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService, @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -153,6 +155,9 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo item.emitter.dispose(); item.provider.dispose(); } + + dispose(this._editorEventListenersMapping.values()); + dispose(this._documentEventListenersMapping.values()); } async $tryApplyEdits(_viewType: string, resource: UriComponents, modelVersionId: number, cellEdits: ICellEditOperation[]): Promise { @@ -200,6 +205,22 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } registerListeners() { + + // forward changes to dirty state + // todo@bpasero this seem way too complicated... is there an easy way to + // the actual resource from a working copy? + this._register(this._workingCopyService.onDidChangeDirty(e => { + if (e.resource.scheme !== Schemas.vscodeNotebook) { + return; + } + for (const notebook of this._notebookService.getNotebookTextModels()) { + if (isEqual(notebook.uri.with({ scheme: Schemas.vscodeNotebook }), e.resource)) { + this._proxy.$acceptDirtyStateChanged(notebook.uri, e.isDirty()); + break; + } + } + })); + this._notebookService.listNotebookEditors().forEach((e) => { this._addNotebookEditor(e); }); @@ -339,26 +360,26 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo })); const updateOrder = () => { - let userOrder = this.configurationService.getValue(DisplayOrderKey); + let userOrder = this._configurationService.getValue(DisplayOrderKey); this._proxy.$acceptDisplayOrder({ - defaultOrder: this.accessibilityService.isScreenReaderOptimized() ? ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER : NOTEBOOK_DISPLAY_ORDER, + defaultOrder: this._accessibilityService.isScreenReaderOptimized() ? ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER : NOTEBOOK_DISPLAY_ORDER, userOrder: userOrder }); }; updateOrder(); - this._register(this.configurationService.onDidChangeConfiguration(e => { + this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectedKeys.indexOf(DisplayOrderKey) >= 0) { updateOrder(); } })); - this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => { + this._register(this._accessibilityService.onDidChangeScreenReaderOptimized(() => { updateOrder(); })); - const activeEditorPane = this.editorService.activeEditorPane as any | undefined; + const activeEditorPane = this._editorService.activeEditorPane as any | undefined; const notebookEditor = activeEditorPane?.isNotebookEditor ? activeEditorPane.getControl() : undefined; this._updateState(notebookEditor); } @@ -371,7 +392,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo }), )); - const activeEditorPane = this.editorService.activeEditorPane as any | undefined; + const activeEditorPane = this._editorService.activeEditorPane as any | undefined; const notebookEditor = activeEditorPane?.isNotebookEditor ? activeEditorPane.getControl() : undefined; this._updateState(notebookEditor); } @@ -391,7 +412,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo private async _updateState(focusedNotebookEditor?: IEditor) { let activeEditor: string | null = null; - const activeEditorPane = this.editorService.activeEditorPane as any | undefined; + const activeEditorPane = this._editorService.activeEditorPane as any | undefined; if (activeEditorPane?.isNotebookEditor) { const notebookEditor = (activeEditorPane.getControl() as INotebookEditor); activeEditor = notebookEditor && notebookEditor.hasModel() ? notebookEditor!.getId() : null; @@ -408,7 +429,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo }); const visibleEditorsMap = new Map(); - this.editorService.visibleEditorPanes.forEach(editor => { + this._editorService.visibleEditorPanes.forEach(editor => { if ((editor as any).isNotebookEditor) { const nbEditorWidget = (editor as any).getControl() as INotebookEditor; if (nbEditorWidget && editors.has(nbEditorWidget.getId())) { @@ -499,7 +520,6 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo const disposable = this._notebookService.registerNotebookController(viewType, extension, controller); this._notebookProviders.set(viewType, { controller, disposable }); - return; } async $updateNotebookProviderOptions(viewType: string, options?: { transientOutputs: boolean; transientMetadata: TransientMetadata; }): Promise { @@ -550,15 +570,15 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo preloads: dto.preloads?.map(u => URI.revive(u)), supportedLanguages: dto.supportedLanguages, resolve: (uri: URI, editorId: string, token: CancellationToken): Promise => { - this.logService.debug('MainthreadNotebooks.resolveNotebookKernel', uri.path, dto.friendlyId); + this._logService.debug('MainthreadNotebooks.resolveNotebookKernel', uri.path, dto.friendlyId); return this._proxy.$resolveNotebookKernel(handle, editorId, uri, dto.friendlyId, token); }, executeNotebookCell: (uri: URI, cellHandle: number | undefined): Promise => { - this.logService.debug('MainthreadNotebooks.executeNotebookCell', uri.path, dto.friendlyId, cellHandle); + this._logService.debug('MainthreadNotebooks.executeNotebookCell', uri.path, dto.friendlyId, cellHandle); return this._proxy.$executeNotebookKernelFromProvider(handle, uri, dto.friendlyId, cellHandle); }, cancelNotebookCell: (uri: URI, cellHandle: number | undefined): Promise => { - this.logService.debug('MainthreadNotebooks.cancelNotebookCell', uri.path, dto.friendlyId, cellHandle); + this._logService.debug('MainthreadNotebooks.cancelNotebookCell', uri.path, dto.friendlyId, cellHandle); return this._proxy.$cancelNotebookKernelFromProvider(handle, uri, dto.friendlyId, cellHandle); } }); @@ -654,7 +674,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo if (statusBarEntry.visible) { this._cellStatusBarEntries.set( id, - this.cellStatusBarService.addEntry(statusBarEntry)); + this._cellStatusBarService.addEntry(statusBarEntry)); } } @@ -678,23 +698,23 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo override: false, }; - const columnArg = viewColumnToEditorGroup(this._editorGroupService, options.position); + const columnArg = viewColumnToEditorGroup(this._editorGroupsService, options.position); let group: IEditorGroup | undefined = undefined; if (columnArg === SIDE_GROUP) { - const direction = preferredSideBySideGroupDirection(this.configurationService); + const direction = preferredSideBySideGroupDirection(this._configurationService); - let neighbourGroup = this.editorGroupsService.findGroup({ direction }); + let neighbourGroup = this._editorGroupsService.findGroup({ direction }); if (!neighbourGroup) { - neighbourGroup = this.editorGroupsService.addGroup(this.editorGroupsService.activeGroup, direction); + neighbourGroup = this._editorGroupsService.addGroup(this._editorGroupsService.activeGroup, direction); } group = neighbourGroup; } else { - group = this.editorGroupsService.getGroup(viewColumnToEditorGroup(this.editorGroupsService, columnArg)) ?? this.editorGroupsService.activeGroup; + group = this._editorGroupsService.getGroup(viewColumnToEditorGroup(this._editorGroupsService, columnArg)) ?? this._editorGroupsService.activeGroup; } - const input = this.editorService.createEditorInput({ resource: URI.revive(resource), options: editorOptions }); + const input = this._editorService.createEditorInput({ resource: URI.revive(resource), options: editorOptions }); // TODO: handle options.selection const editorPane = await this._instantiationService.invokeFunction(openEditorWith, input, viewType, options, group); diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index 95fc3e9ae7f..110becb9583 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -6,6 +6,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; +import { Range } from 'vs/editor/common/core/range'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { getTestSubscriptionKey, ITestState, RunTestsRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import { ITestResultService, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResultService'; @@ -18,6 +19,7 @@ const reviveDiff = (diff: TestsDiff) => { const item = entry[1]; if (item.item.location) { item.item.location.uri = URI.revive(item.item.location.uri); + item.item.location.range = Range.lift(item.item.location.range); } } } @@ -71,6 +73,7 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh for (const message of state.messages) { if (message.location) { message.location.uri = URI.revive(message.location.uri); + message.location.range = Range.lift(message.location.range); } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 09a44ef2b07..b9bacd2d7f6 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1253,6 +1253,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // checkProposedApiEnabled(extension); return extHostTypes.TimelineItem; }, + get NotebookCellRange() { + return extHostTypes.NotebookCellRange; + }, get NotebookCellKind() { // checkProposedApiEnabled(extension); return extHostTypes.NotebookCellKind; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index ff5e676f9cb..e23c5c0e46a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1125,8 +1125,7 @@ export interface ExtHostLabelServiceShape { } export interface ExtHostAuthenticationShape { - $getAllSessions(id: string): Promise>; - $getSessions(id: string, scopes: string[]): Promise>; + $getSessions(id: string, scopes?: string[]): Promise>; $createSession(id: string, scopes: string[]): Promise; $removeSession(id: string, sessionId: string): Promise; $onDidChangeAuthenticationSessions(id: string, label: string, event: modes.AuthenticationSessionsChangeEvent): Promise; @@ -1732,13 +1731,8 @@ export interface INotebookSelectionChangeEvent { selections: number[]; } -export interface INotebookCellVisibleRange { - start: number; - end: number; -} - export interface INotebookVisibleRangesEvent { - ranges: INotebookCellVisibleRange[]; + ranges: ICellRange[]; } export interface INotebookEditorPropertiesChangeData { @@ -1804,6 +1798,7 @@ export interface ExtHostNotebookShape { $acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelFriendlyId: string | undefined }): void; $onDidReceiveMessage(editorId: string, rendererId: string | undefined, message: unknown): void; $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEventDto, isDirty: boolean): void; + $acceptDirtyStateChanged(uriComponents: UriComponents, isDirty: boolean): void; $acceptModelSaved(uriComponents: UriComponents): void; $acceptEditorPropertiesChanged(id: string, data: INotebookEditorPropertiesChangeData): void; $acceptDocumentPropertiesChanged(uriComponents: UriComponents, data: INotebookDocumentPropertiesChangeData): void; diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index 988f93a5c9f..b4445864a7c 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -147,16 +147,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { throw new Error(`Unable to find authentication provider with handle: ${providerId}`); } - $getAllSessions(providerId: string): Promise> { - const providerData = this._authenticationProviders.get(providerId); - if (providerData) { - return Promise.resolve(providerData.provider.getAllSessions()); - } - - throw new Error(`Unable to find authentication provider with handle: ${providerId}`); - } - - $getSessions(providerId: string, scopes: string[]): Promise> { + $getSessions(providerId: string, scopes?: string[]): Promise> { const providerData = this._authenticationProviders.get(providerId); if (providerData) { return Promise.resolve(providerData.provider.getSessions(scopes)); diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 40ff93d7dd5..6b995a33914 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -403,7 +403,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN resolvedOptions = { position: typeConverters.ViewColumn.from(options.viewColumn), preserveFocus: options.preserveFocus, - selection: options.selection, + selection: options.selection && typeConverters.NotebookCellRange.from(options.selection), pinned: typeof options.preview === 'boolean' ? !options.preview : undefined }; } else { @@ -578,7 +578,14 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } } - public $acceptModelSaved(uriComponents: UriComponents): void { + $acceptDirtyStateChanged(resource: UriComponents, isDirty: boolean): void { + const document = this._documents.get(URI.revive(resource)); + if (document) { + document.acceptModelChanged({ rawEvents: [], versionId: document.notebookDocument.version }, isDirty); + } + } + + $acceptModelSaved(uriComponents: UriComponents): void { const document = this._documents.get(URI.revive(uriComponents)); if (document) { // this.$acceptDirtyStateChanged(uriComponents, false); @@ -601,7 +608,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } if (data.visibleRanges) { - editor.editor._acceptVisibleRanges(data.visibleRanges.ranges); + editor.editor._acceptVisibleRanges(data.visibleRanges.ranges.map(typeConverters.NotebookCellRange.to)); this._onDidChangeNotebookEditorVisibleRanges.fire({ notebookEditor: editor.editor, visibleRanges: editor.editor.visibleRanges @@ -636,7 +643,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } } - private _createExtHostEditor(document: ExtHostNotebookDocument, editorId: string, selections: number[], visibleRanges: vscode.NotebookCellRange[]) { + private _createExtHostEditor(document: ExtHostNotebookDocument, editorId: string, selections: number[], visibleRanges: extHostTypes.NotebookCellRange[]) { const revivedUri = document.uri; let webComm = this._webviewComm.get(editorId); @@ -722,7 +729,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN emitDocumentMetadataChange(event: vscode.NotebookDocumentMetadataChangeEvent): void { that._onDidChangeNotebookDocumentMetadata.fire(event); } - }, viewType, modelData.contentOptions, { ...notebookDocumentMetadataDefaults, ...modelData.metadata }, uri, storageRoot); + }, viewType, modelData.contentOptions, typeConverters.NotebookDocumentMetadata.to(modelData.metadata ?? {}), uri, storageRoot); document.acceptModelChanged({ versionId: modelData.versionId, @@ -746,7 +753,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN // create editor if populated if (modelData.attachedEditor) { - this._createExtHostEditor(document, modelData.attachedEditor.id, modelData.attachedEditor.selections, modelData.attachedEditor.visibleRanges); + this._createExtHostEditor(document, modelData.attachedEditor.id, modelData.attachedEditor.selections, modelData.attachedEditor.visibleRanges.map(typeConverters.NotebookCellRange.to)); editorChanged = true; } @@ -766,7 +773,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const document = this._documents.get(revivedUri); if (document) { - this._createExtHostEditor(document, editorModelData.id, editorModelData.selections, editorModelData.visibleRanges); + this._createExtHostEditor(document, editorModelData.id, editorModelData.selections, editorModelData.visibleRanges.map(typeConverters.NotebookCellRange.to)); editorChanged = true; } } diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index 21d2e99a563..e9e715885c4 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -8,29 +8,29 @@ import { hash } from 'vs/base/common/hash'; import { Disposable, DisposableStore, dispose } 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 } from 'vs/base/common/uri'; import { CellKind, INotebookDocumentPropertiesChangeData } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocumentsAndEditors, IExtHostModelAddedData } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import * as extHostTypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; +import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { IMainCellDto, IOutputDto, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookCellsSplice2, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import * as vscode from 'vscode'; class RawContentChangeEvent { - constructor(readonly start: number, readonly deletedCount: number, readonly deletedItems: ExtHostCell[], readonly items: ExtHostCell[]) { } + constructor(readonly start: number, readonly deletedCount: number, readonly deletedItems: vscode.NotebookCell[], readonly items: ExtHostCell[]) { } static asApiEvent(event: RawContentChangeEvent): vscode.NotebookCellsChangeData { return Object.freeze({ start: event.start, deletedCount: event.deletedCount, - deletedItems: event.deletedItems.map(data => data.cell), + deletedItems: event.deletedItems, items: event.items.map(data => data.cell) }); } } -export class ExtHostCell extends Disposable { +export class ExtHostCell { static asModelAddData(notebook: vscode.NotebookDocument, cell: IMainCellDto): IExtHostModelAddedData { return { @@ -47,12 +47,8 @@ export class ExtHostCell extends Disposable { private _onDidDispose = new Emitter(); readonly onDidDispose: Event = this._onDidDispose.event; - private _onDidChangeOutputs = new Emitter[]>(); - readonly onDidChangeOutputs: Event[]> = this._onDidChangeOutputs.event; - private _outputs: IOutputDto[]; - - private _metadata: vscode.NotebookCellMetadata; + private _metadata: extHostTypes.NotebookCellMetadata; readonly handle: number; readonly uri: URI; @@ -65,16 +61,16 @@ export class ExtHostCell extends Disposable { private readonly _extHostDocument: ExtHostDocumentsAndEditors, private readonly _cellData: IMainCellDto, ) { - super(); - this.handle = _cellData.handle; this.uri = URI.revive(_cellData.uri); this.cellKind = _cellData.cellKind; - this._outputs = _cellData.outputs; + this._metadata = extHostTypeConverters.NotebookCellMetadata.to(_cellData.metadata ?? {}); + } - - this._metadata = _cellData.metadata ?? {}; + dispose() { + this._onDidDispose.fire(); + this._onDidDispose.dispose(); } get cell(): vscode.NotebookCell { @@ -100,17 +96,12 @@ export class ExtHostCell extends Disposable { return this._cell; } - dispose() { - super.dispose(); - this._onDidDispose.fire(); - } - setOutputs(newOutputs: IOutputDto[]): void { this._outputs = newOutputs; } - setMetadata(newMetadata: vscode.NotebookCellMetadata): void { - this._metadata = newMetadata; + setMetadata(newMetadata: NotebookCellMetadata): void { + this._metadata = extHostTypeConverters.NotebookCellMetadata.to(newMetadata); } } @@ -137,10 +128,8 @@ export class ExtHostNotebookDocument extends Disposable { private _cellDisposableMapping = new Map(); private _notebook: vscode.NotebookDocument | undefined; - // private _metadata: Required; - // private _metadataChangeListener: IDisposable; private _versionId = 0; - private _isDirty: boolean = false; + private _isDirty = false; private _backupCounter = 1; private _backup?: vscode.NotebookDocumentBackup; private _disposed = false; @@ -150,7 +139,7 @@ export class ExtHostNotebookDocument extends Disposable { private readonly _emitter: INotebookEventEmitter, private readonly _viewType: string, private readonly _contentOptions: vscode.NotebookDocumentContentOptions, - private _metadata: Required, + private _metadata: extHostTypes.NotebookDocumentMetadata, public readonly uri: URI, private readonly _storagePath: URI | undefined ) { @@ -206,7 +195,7 @@ export class ExtHostNotebookDocument extends Disposable { ...notebookDocumentMetadataDefaults, ...data.metadata }; - this._metadata = newMetadata; + this._metadata = this._metadata.with(newMetadata); this._emitter.emitDocumentMetadataChange({ document: this.notebookDocument }); } @@ -263,12 +252,14 @@ export class ExtHostNotebookDocument extends Disposable { this._cellDisposableMapping.delete(this._cells[j].handle); } + const changeEvent = new RawContentChangeEvent(splice[0], splice[1], [], newCells); const deletedItems = this._cells.splice(splice[0], splice[1], ...newCells); for (let cell of deletedItems) { removedCellDocuments.push(cell.uri); + changeEvent.deletedItems.push(cell.cell); } - contentChangeEvents.push(new RawContentChangeEvent(splice[0], splice[1], deletedItems, newCells)); + contentChangeEvents.push(changeEvent); }); this._documentsAndEditors.acceptDocumentsAndEditorsDelta({ diff --git a/src/vs/workbench/api/common/extHostNotebookEditor.ts b/src/vs/workbench/api/common/extHostNotebookEditor.ts index 9afae7d5be3..7b3105cfb18 100644 --- a/src/vs/workbench/api/common/extHostNotebookEditor.ts +++ b/src/vs/workbench/api/common/extHostNotebookEditor.ts @@ -90,7 +90,7 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook //TODO@rebornix noop setter? selection?: vscode.NotebookCell; - private _visibleRanges: vscode.NotebookCellRange[] = []; + private _visibleRanges: extHostTypes.NotebookCellRange[] = []; private _viewColumn?: vscode.ViewColumn; private _active: boolean = false; private _visible: boolean = false; @@ -153,11 +153,11 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook return this._visibleRanges; } - set visibleRanges(_range: vscode.NotebookCellRange[]) { + set visibleRanges(_range) { throw readonly('visibleRanges'); } - _acceptVisibleRanges(value: vscode.NotebookCellRange[]): void { + _acceptVisibleRanges(value: extHostTypes.NotebookCellRange[]): void { this._visibleRanges = value; } @@ -233,13 +233,13 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook return this._proxy.$trySetDecorations( this.id, - range, + extHostConverter.NotebookCellRange.from(range), decorationType.key ); } revealRange(range: vscode.NotebookCellRange, revealType?: extHostTypes.NotebookEditorRevealType) { - this._proxy.$tryRevealRange(this.id, range, revealType || extHostTypes.NotebookEditorRevealType.Default); + this._proxy.$tryRevealRange(this.id, extHostConverter.NotebookCellRange.from(range), revealType ?? extHostTypes.NotebookEditorRevealType.Default); } async postMessage(message: any): Promise { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 9799bddf7bd..910683b56be 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -31,7 +31,7 @@ import { coalesce, isNonEmptyArray } from 'vs/base/common/arrays'; import { RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; -import { CellEditType, CellKind, ICellDto2, INotebookDecorationRenderOptions, IOutputDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITestItem, ITestState } from 'vs/workbench/contrib/testing/common/testCollection'; export interface PositionLike { @@ -509,7 +509,7 @@ export namespace TextEdit { } export namespace WorkspaceEdit { - export function from(value: vscode.WorkspaceEdit, documents?: ExtHostDocumentsAndEditors, notebooks?: ExtHostNotebookController): extHostProtocol.IWorkspaceEditDto { + export function from(value: vscode.WorkspaceEdit, documents?: ExtHostDocumentsAndEditors, extHostNotebooks?: ExtHostNotebookController): extHostProtocol.IWorkspaceEditDto { const result: extHostProtocol.IWorkspaceEditDto = { edits: [] }; @@ -544,7 +544,7 @@ export namespace WorkspaceEdit { resource: entry.uri, edit: entry.edit, notebookMetadata: entry.notebookMetadata, - notebookVersionId: notebooks?.lookupNotebookDocument(entry.uri)?.notebookDocument.version + notebookVersionId: extHostNotebooks?.lookupNotebookDocument(entry.uri)?.notebookDocument.version }); } else if (entry._type === types.FileEditType.CellOutput) { @@ -554,7 +554,7 @@ export namespace WorkspaceEdit { metadata: entry.metadata, resource: entry.uri, edit: { - editType: CellEditType.Output, + editType: notebooks.CellEditType.Output, index: entry.index, append: entry.append, outputs: entry.newOutputs.map(NotebookCellOutput.from) @@ -568,7 +568,7 @@ export namespace WorkspaceEdit { metadata: entry.metadata, resource: entry.uri, edit: { - editType: CellEditType.Metadata, + editType: notebooks.CellEditType.Metadata, index: entry.index, metadata: entry.newMetadata } @@ -579,9 +579,9 @@ export namespace WorkspaceEdit { _type: extHostProtocol.WorkspaceEditType.Cell, metadata: entry.metadata, resource: entry.uri, - notebookVersionId: notebooks?.lookupNotebookDocument(entry.uri)?.notebookDocument.version, + notebookVersionId: extHostNotebooks?.lookupNotebookDocument(entry.uri)?.notebookDocument.version, edit: { - editType: CellEditType.Replace, + editType: notebooks.CellEditType.Replace, index: entry.index, count: entry.count, cells: entry.cells.map(NotebookCellData.from) @@ -593,7 +593,7 @@ export namespace WorkspaceEdit { metadata: entry.metadata, resource: entry.uri, edit: { - editType: CellEditType.OutputItems, + editType: notebooks.CellEditType.OutputItems, index: entry.index, outputId: entry.outputId, items: entry.newOutputItems?.map(item => ({ mime: item.mime, value: item.value, metadata: item.metadata })) || [], @@ -1344,22 +1344,52 @@ export namespace LanguageSelector { } } +export namespace NotebookCellRange { + + export function from(range: vscode.NotebookCellRange): notebooks.ICellRange { + return { start: range.start, end: range.end }; + } + + export function to(range: notebooks.ICellRange): types.NotebookCellRange { + return new types.NotebookCellRange(range.start, range.end); + } +} + +export namespace NotebookCellMetadata { + + export function to(data: vscode.NotebookCellMetadata): types.NotebookCellMetadata { + return new types.NotebookCellMetadata(data.editable, data.breakpointMargin, data.runnable, data.hasExecutionOrder, data.executionOrder, data.runStartTime, data.runStartTime, data.statusMessage, data.lastRunDuration, data.inputCollapsed, data.outputCollapsed, data.custom); + } +} + +export namespace NotebookDocumentMetadata { + + export function from(data: types.NotebookDocumentMetadata): notebooks.NotebookDocumentMetadata { + return data; + } + + export function to(data: vscode.NotebookDocumentMetadata): types.NotebookDocumentMetadata { + return new types.NotebookDocumentMetadata(data.editable, data.runnable, data.cellEditable, data.cellRunnable, data.cellHasExecutionOrder, data.displayOrder, data.custom, data.runState, data.trusted); + } + +} + export namespace NotebookCellKind { - export function from(data: vscode.NotebookCellKind): CellKind { + export function from(data: vscode.NotebookCellKind): notebooks.CellKind { switch (data) { case types.NotebookCellKind.Markdown: - return CellKind.Markdown; + return notebooks.CellKind.Markdown; case types.NotebookCellKind.Code: default: - return CellKind.Code; + return notebooks.CellKind.Code; } } - export function to(data: CellKind): vscode.NotebookCellKind { + export function to(data: notebooks.CellKind): vscode.NotebookCellKind { switch (data) { - case CellKind.Markdown: + case notebooks.CellKind.Markdown: return types.NotebookCellKind.Markdown; - case CellKind.Code: + case notebooks.CellKind.Code: default: return types.NotebookCellKind.Code; } @@ -1368,7 +1398,7 @@ export namespace NotebookCellKind { export namespace NotebookCellData { - export function from(data: vscode.NotebookCellData): ICellDto2 { + export function from(data: vscode.NotebookCellData): notebooks.ICellDto2 { return { cellKind: NotebookCellKind.from(data.cellKind), language: data.language, @@ -1386,7 +1416,7 @@ export namespace NotebookCellData { } export namespace NotebookCellOutput { - export function from(output: types.NotebookCellOutput): IOutputDto { + export function from(output: types.NotebookCellOutput): notebooks.IOutputDto { const data = Object.create(null); const custom = Object.create(null); @@ -1407,7 +1437,7 @@ export namespace NotebookCellOutput { }; } - export function to(output: IOutputDto): vscode.NotebookCellOutput { + export function to(output: notebooks.IOutputDto): vscode.NotebookCellOutput { const items: types.NotebookCellOutputItem[] = output.outputs.map(op => new types.NotebookCellOutputItem(op.mime, op.value, op.metadata)); return new types.NotebookCellOutput(items, output.outputId); } @@ -1488,7 +1518,7 @@ export namespace NotebookExclusiveDocumentPattern { } export namespace NotebookDecorationRenderOptions { - export function from(options: vscode.NotebookDecorationRenderOptions): INotebookDecorationRenderOptions { + export function from(options: vscode.NotebookDecorationRenderOptions): notebooks.INotebookDecorationRenderOptions { return { backgroundColor: options.backgroundColor, borderColor: options.borderColor, @@ -1507,7 +1537,7 @@ export namespace TestState { severity: message.severity, expectedOutput: message.expectedOutput, actualOutput: message.actualOutput, - location: message.location ? location.from(message.location) : undefined, + location: message.location ? location.from(message.location) as any : undefined, })) ?? [], }; } @@ -1536,7 +1566,7 @@ export namespace TestItem { return { extId: item.id ?? (parentExtId ? `${parentExtId}\0${item.label}` : item.label), label: item.label, - location: item.location ? location.from(item.location) : undefined, + location: item.location ? location.from(item.location) as any : undefined, debuggable: item.debuggable ?? false, description: item.description, runnable: item.runnable ?? true, diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 96a66824ee6..9bc85fa2ace 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2849,6 +2849,32 @@ export enum ColorThemeKind { //#region Notebook +export class NotebookCellRange { + + private _start: number; + private _end: number; + + get start() { + return this._start; + } + + get end() { + return this._end; + } + + constructor(start: number, end: number) { + // todo@rebornix + // if (start < 0) { + // throw illegalArgument('start must be positive'); + // } + // if (end < start) { + // throw illegalArgument('end cannot be smaller than start'); + // } + this._start = start; + this._end = end; + } +} + export class NotebookCellMetadata { constructor( diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index c3f7817a47f..8b62c5a88de 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -210,7 +210,7 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { const providers = this.authenticationService.getProviderIds(); const allSessions = providers.map(async providerId => { try { - const sessions = await this.authenticationService.getAllSessions(providerId); + const sessions = await this.authenticationService.getSessions(providerId); const groupedSessions: { [label: string]: AuthenticationSession[]; } = {}; sessions.forEach(session => { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index ba37e0b668f..f9ee130df8c 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -534,8 +534,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { orientation: ActionsOrientation.VERTICAL, ariaLabel: localize('manage', "Manage"), animated: false, - preventLoopNavigation: true, - ignoreOrientationForPreviousAndNextKey: true + preventLoopNavigation: true })); this.globalActivityAction = this._register(new ActivityAction({ diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 70d629fb2c0..94fccfcb646 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -227,7 +227,6 @@ export class CompositeBar extends Widget implements ICompositeBar { ariaLabel: nls.localize('activityBarAriaLabel', "Active View Switcher"), animated: false, preventLoopNavigation: this.options.preventLoopNavigation, - ignoreOrientationForPreviousAndNextKey: true, triggerKeys: { keyDown: true } })); diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 003527d67e0..78d51af076a 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -464,7 +464,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCo MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCommands.SPLIT_EDITOR_RIGHT, title: nls.localize('splitRight', "Split Right") }, group: '5_split', order: 40 }); // Editor Title Menu -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.TOGGLE_DIFF_SIDE_BY_SIDE, title: nls.localize('toggleInlineView', "Toggle Inline View") }, group: '1_diff', order: 10, when: ContextKeyExpr.has('isInDiffEditor') }); +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.TOGGLE_DIFF_SIDE_BY_SIDE, title: nls.localize('inlineView', "Inline View"), toggled: ContextKeyExpr.equals('config.diffEditor.renderSideBySide', false) }, group: '1_diff', order: 10, when: ContextKeyExpr.has('isInDiffEditor') }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.SHOW_EDITORS_IN_GROUP, title: nls.localize('showOpenedEditors', "Show Opened Editors") }, group: '3_open', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.CLOSE_EDITORS_IN_GROUP_COMMAND_ID, title: nls.localize('closeAll', "Close All") }, group: '5_close', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.CLOSE_SAVED_EDITORS_COMMAND_ID, title: nls.localize('closeAllSaved', "Close Saved") }, group: '5_close', order: 20 }); diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index d07cc642e0a..850294ee6a0 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -509,8 +509,8 @@ export class TabsTitleControl extends TitleControl { setActive(isGroupActive: boolean): void { // Activity has an impact on each tab's active indication - this.forEachTab((editor, index, tabContainer, tabLabelWidget, tabLabel) => { - this.redrawTabActiveAndDirty(isGroupActive, editor, tabContainer, tabLabelWidget); + this.forEachTab((editor, index, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => { + this.redrawTabActiveAndDirty(isGroupActive, editor, tabContainer, tabActionBar); }); // Activity has an impact on the toolbar, so we need to update and layout @@ -545,7 +545,7 @@ export class TabsTitleControl extends TitleControl { } updateEditorDirty(editor: IEditorInput): void { - this.withTab(editor, (editor, index, tabContainer, tabLabelWidget) => this.redrawTabActiveAndDirty(this.accessor.activeGroup === this.group, editor, tabContainer, tabLabelWidget)); + this.withTab(editor, (editor, index, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => this.redrawTabActiveAndDirty(this.accessor.activeGroup === this.group, editor, tabContainer, tabActionBar)); } updateOptions(oldOptions: IEditorPartOptions, newOptions: IEditorPartOptions): void { @@ -606,7 +606,6 @@ export class TabsTitleControl extends TitleControl { // Tab Container const tabContainer = document.createElement('div'); tabContainer.draggable = true; - tabContainer.tabIndex = 0; tabContainer.setAttribute('role', 'tab'); tabContainer.classList.add('tab'); @@ -1120,7 +1119,7 @@ export class TabsTitleControl extends TitleControl { this.redrawTabBorders(index, tabContainer); // Active / dirty state - this.redrawTabActiveAndDirty(this.accessor.activeGroup === this.group, editor, tabContainer, tabLabelWidget); + this.redrawTabActiveAndDirty(this.accessor.activeGroup === this.group, editor, tabContainer, tabActionBar); } private redrawTabLabel(editor: IEditorInput, index: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel): void { @@ -1178,15 +1177,15 @@ export class TabsTitleControl extends TitleControl { } } - private redrawTabActiveAndDirty(isGroupActive: boolean, editor: IEditorInput, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel): void { + private redrawTabActiveAndDirty(isGroupActive: boolean, editor: IEditorInput, tabContainer: HTMLElement, tabActionBar: ActionBar): void { const isTabActive = this.group.isActive(editor); const hasModifiedBorderTop = this.doRedrawTabDirty(isGroupActive, isTabActive, editor, tabContainer); - this.doRedrawTabActive(isGroupActive, !hasModifiedBorderTop, editor, tabContainer, tabLabelWidget); + this.doRedrawTabActive(isGroupActive, !hasModifiedBorderTop, editor, tabContainer, tabActionBar); } - private doRedrawTabActive(isGroupActive: boolean, allowBorderTop: boolean, editor: IEditorInput, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel): void { + private doRedrawTabActive(isGroupActive: boolean, allowBorderTop: boolean, editor: IEditorInput, tabContainer: HTMLElement, tabActionBar: ActionBar): void { // Tab is active if (this.group.isActive(editor)) { @@ -1194,6 +1193,7 @@ export class TabsTitleControl extends TitleControl { // Container tabContainer.classList.add('active'); tabContainer.setAttribute('aria-selected', 'true'); + tabContainer.tabIndex = 0; // Only active tab can be focused into tabContainer.style.backgroundColor = this.getColor(isGroupActive ? TAB_ACTIVE_BACKGROUND : TAB_UNFOCUSED_ACTIVE_BACKGROUND) || ''; const activeTabBorderColorBottom = this.getColor(isGroupActive ? TAB_ACTIVE_BORDER : TAB_UNFOCUSED_ACTIVE_BORDER); @@ -1216,6 +1216,9 @@ export class TabsTitleControl extends TitleControl { // Label tabContainer.style.color = this.getColor(isGroupActive ? TAB_ACTIVE_FOREGROUND : TAB_UNFOCUSED_ACTIVE_FOREGROUND) || ''; + + // Actions + tabActionBar.setFocusable(true); } // Tab is inactive @@ -1224,11 +1227,15 @@ export class TabsTitleControl extends TitleControl { // Container tabContainer.classList.remove('active'); tabContainer.setAttribute('aria-selected', 'false'); + tabContainer.tabIndex = -1; // Only active tab can be focused into tabContainer.style.backgroundColor = this.getColor(isGroupActive ? TAB_INACTIVE_BACKGROUND : TAB_UNFOCUSED_INACTIVE_BACKGROUND) || ''; tabContainer.style.boxShadow = ''; // Label tabContainer.style.color = this.getColor(isGroupActive ? TAB_INACTIVE_FOREGROUND : TAB_UNFOCUSED_INACTIVE_FOREGROUND) || ''; + + // Actions + tabActionBar.setFocusable(false); } } diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index f3587a51a51..96f49a70965 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -127,13 +127,13 @@ export abstract class MenubarControl extends Disposable { this.updateService.onStateChange(() => this.onUpdateStateChange()); // Listen for changes in recently opened menu - this._register(this.workspacesService.onRecentlyOpenedChange(() => { this.onRecentlyOpenedChange(); })); + this._register(this.workspacesService.onDidChangeRecentlyOpened(() => { this.onDidChangeRecentlyOpened(); })); // Listen to keybindings change this._register(this.keybindingService.onDidUpdateKeybindings(() => this.updateMenubar())); // Update recent menu items on formatter registration - this._register(this.labelService.onDidChangeFormatters(() => { this.onRecentlyOpenedChange(); })); + this._register(this.labelService.onDidChangeFormatters(() => { this.onDidChangeRecentlyOpened(); })); } protected updateMenubar(): void { @@ -189,7 +189,7 @@ export abstract class MenubarControl extends Disposable { protected onDidChangeWindowFocus(hasFocus: boolean): void { // When we regain focus, update the recent menu items if (hasFocus) { - this.onRecentlyOpenedChange(); + this.onDidChangeRecentlyOpened(); } } @@ -205,7 +205,7 @@ export abstract class MenubarControl extends Disposable { // Since we try not update when hidden, we should // try to update the recently opened list on visibility changes if (event.affectsConfiguration('window.menuBarVisibility')) { - this.onRecentlyOpenedChange(); + this.onDidChangeRecentlyOpened(); } } @@ -213,7 +213,7 @@ export abstract class MenubarControl extends Disposable { return isMacintosh && isNative ? false : getMenuBarVisibility(this.configurationService) === 'hidden'; } - protected onRecentlyOpenedChange(): void { + protected onDidChangeRecentlyOpened(): void { // Do not update recently opened when the menubar is hidden #108712 if (!this.menubarHidden) { @@ -543,7 +543,7 @@ export class CustomMenubarControl extends MenubarControl { private onDidVisibilityChange(visible: boolean): void { this.visible = visible; - this.onRecentlyOpenedChange(); + this.onDidChangeRecentlyOpened(); this._onVisibilityChange.fire(visible); } @@ -765,12 +765,12 @@ export class CustomMenubarControl extends MenubarControl { super.onUpdateStateChange(); } - protected onRecentlyOpenedChange(): void { + protected onDidChangeRecentlyOpened(): void { if (!this.visible) { return; } - super.onRecentlyOpenedChange(); + super.onDidChangeRecentlyOpened(); } protected onUpdateKeybindings(): void { diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 9c49ffdcca2..f8961757153 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -873,7 +873,8 @@ class TreeRenderer extends Disposable implements ITreeRenderer { // Need to react with a timeout on the blur event due to possible concurent splices #56443 setTimeout(() => { - if (!template.breakpoint.name) { - wrapUp(true); - } + wrapUp(!!inputBox.value); }); })); diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 407671b15e8..145e9a4b4bc 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -515,7 +515,8 @@ class SessionsRenderer implements ICompressibleTreeRenderer { return action.createActionViewItem(); } return new ExtensionActionViewItem(null, action, actionOptions); - } + }, + respectOrientationForPreviousAndNextKey: true }); actionbar.onDidRun(({ error }) => error && this.notificationService.error(error)); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 59b05dd4866..451dcceccff 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -183,7 +183,8 @@ export class ExplorerView extends ViewPane { @IClipboardService private clipboardService: IClipboardService, @IFileService private readonly fileService: IFileService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @IOpenerService openerService: IOpenerService, + @ICommandService private readonly commandService: ICommandService, + @IOpenerService openerService: IOpenerService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); @@ -459,6 +460,13 @@ export class ExplorerView extends ViewPane { } })); + this._register(this.tree.onMouseDblClick(e => { + if (e.element === null) { + // click in empty area -> create a new file #116676 + this.commandService.executeCommand(NEW_FILE_COMMAND_ID); + } + })); + // save view state this._register(this.storageService.onWillSaveState(() => { this.storageService.store(ExplorerView.TREE_VIEW_STATE_STORAGE_KEY, JSON.stringify(this.tree.getViewState()), StorageScope.WORKSPACE, StorageTarget.MACHINE); diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index e54534fd378..9936ef5a754 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -537,7 +537,7 @@ class EditorGroupRenderer implements IListRenderer session.scopes.includes('repo')); const theme = this.themeService.getColorTheme(); const issueReporterData: IssueReporterData = Object.assign({ diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 653b012f4b2..c3c14d69e1d 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -302,7 +302,8 @@ class MarkerWidget extends Disposable { ) { super(); this.actionBar = this._register(new ActionBar(dom.append(parent, dom.$('.actions')), { - actionViewItemProvider: (action: IAction) => action.id === QuickFixAction.ID ? _instantiationService.createInstance(QuickFixActionViewItem, action) : undefined + actionViewItemProvider: (action: IAction) => action.id === QuickFixAction.ID ? _instantiationService.createInstance(QuickFixActionViewItem, action) : undefined, + respectOrientationForPreviousAndNextKey: true })); this.icon = dom.append(parent, dom.$('')); this.multilineActionbar = this._register(new ActionBar(dom.append(parent, dom.$('.multiline-actions')), { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index 9705204ab05..26772735238 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -21,7 +21,7 @@ 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 { CATEGORIES } from 'vs/workbench/common/actions'; -import { BaseCellRenderTemplate, CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, EXPAND_CELL_CONTENT_COMMAND_ID, IActiveNotebookEditor, ICellViewModel, INotebookEditor, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, 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 } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { BaseCellRenderTemplate, CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, EXPAND_CELL_CONTENT_COMMAND_ID, IActiveNotebookEditor, ICellViewModel, INotebookEditor, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, 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_KERNEL_COUNT, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { CellEditType, CellKind, ICellEditOperation, ICellRange, INotebookDocumentFilter, isDocumentExcludePattern, NotebookCellMetadata, NotebookCellRunState, NOTEBOOK_EDITOR_CURSOR_BEGIN_END, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -228,10 +228,11 @@ export function getWidgetFromUri(accessor: ServicesAccessor, uri: URI) { return undefined; } -registerAction2(class extends NotebookCellAction { +registerAction2(class ExecuteCell extends NotebookCellAction { constructor() { super({ id: EXECUTE_CELL_COMMAND_ID, + precondition: ContextKeyExpr.or(ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0), NOTEBOOK_CELL_TYPE.isEqualTo('markdown')), title: localize('notebookActions.execute', "Execute Cell"), keybinding: { when: NOTEBOOK_CELL_LIST_FOCUSED, @@ -312,7 +313,7 @@ registerAction2(class extends NotebookCellAction { } }); -registerAction2(class extends NotebookCellAction { +registerAction2(class StopExecuteCell extends NotebookCellAction { constructor() { super({ id: CANCEL_CELL_COMMAND_ID, @@ -442,10 +443,11 @@ export class DeleteCellAction extends MenuItemAction { } } -registerAction2(class extends NotebookCellAction { +registerAction2(class ExecuteCellSelectBelow extends NotebookCellAction { constructor() { super({ id: EXECUTE_CELL_SELECT_BELOW, + precondition: ContextKeyExpr.or(ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0), NOTEBOOK_CELL_TYPE.isEqualTo('markdown')), title: localize('notebookActions.executeAndSelectBelow', "Execute Notebook Cell and Select Below"), keybinding: { when: NOTEBOOK_CELL_LIST_FOCUSED, @@ -478,10 +480,11 @@ registerAction2(class extends NotebookCellAction { } }); -registerAction2(class extends NotebookCellAction { +registerAction2(class ExecuteCellInsertBelow extends NotebookCellAction { constructor() { super({ id: EXECUTE_CELL_INSERT_BELOW, + precondition: ContextKeyExpr.or(ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0), NOTEBOOK_CELL_TYPE.isEqualTo('markdown')), title: localize('notebookActions.executeAndInsertBelow', "Execute Notebook Cell and Insert Below"), keybinding: { when: NOTEBOOK_CELL_LIST_FOCUSED, diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index 235dd429996..24f880fbabf 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -150,7 +150,8 @@ class PropertyHeader extends Disposable { } return undefined; - } + }, + respectOrientationForPreviousAndNextKey: true }); this._register(this._toolbar); this._toolbar.context = { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts index ce2bc9c6ec2..e25db2f37b0 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts @@ -188,7 +188,8 @@ export class CellDiffSideBySideRenderer implements IListRenderer('noteboo export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey('notebookCellOutputIsCollapsed', false); // bool // Kernels export const NOTEBOOK_HAS_MULTIPLE_KERNELS = new RawContextKey('notebookHasMultipleKernels', false); +export const NOTEBOOK_KERNEL_COUNT = new RawContextKey('notebookKernelCount', 0); //#endregion diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts index 80bb8e33897..cd68acce0ec 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts @@ -160,11 +160,12 @@ ${patterns} if (!this._textModel) { this._textModel = await this._notebookModelResolverService.resolve(this.resource, this.viewType!); - - this._register(this._textModel.object.onDidChangeDirty(() => { - this._onDidChangeDirty.fire(); - })); - + if (this.isDisposed()) { + this._textModel.dispose(); + this._textModel = null; + return null; + } + this._register(this._textModel.object.onDidChangeDirty(() => this._onDidChangeDirty.fire())); if (this._textModel.object.isDirty()) { this._onDidChangeDirty.fire(); } @@ -185,10 +186,8 @@ ${patterns} } dispose() { - if (this._textModel) { - this._textModel.dispose(); - this._textModel = null; - } + this._textModel?.dispose(); + this._textModel = null; super.dispose(); } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 148cb42ef38..6f8e12d51b9 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -14,7 +14,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Color, RGBA } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { combinedDisposable, Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { combinedDisposable, Disposable, DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ScrollEvent } from 'vs/base/common/scrollable'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; @@ -41,7 +41,7 @@ import { IEditorMemento } from 'vs/workbench/common/editor'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { PANEL_BORDER } from 'vs/workbench/common/theme'; import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_OUTPUT_PADDING, CELL_RUN_GUTTER, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFocusMode, IActiveNotebookEditor, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IGenericCellViewModel, IInsetRenderOutput, INotebookCellList, INotebookCellOutputLayoutInfo, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, NotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFocusMode, IActiveNotebookEditor, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IGenericCellViewModel, IInsetRenderOutput, INotebookCellList, INotebookCellOutputLayoutInfo, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, NotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { NotebookKernelProviderAssociation, NotebookKernelProviderAssociations, notebookKernelProviderAssociationsSettingId } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; @@ -92,14 +92,16 @@ 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 _editorFocus: IContextKey | null = null; - private _outputFocus: IContextKey | null = null; - private _editorEditable: IContextKey | null = null; - private _editorRunnable: IContextKey | null = null; - private _notebookExecuting: IContextKey | null = null; - private _notebookHasMultipleKernels: IContextKey | null = null; + private readonly _editorFocus: IContextKey; + private readonly _outputFocus: IContextKey; + private readonly _editorEditable: IContextKey; + private readonly _editorRunnable: IContextKey; + private readonly _notebookExecuting: IContextKey; + private readonly _notebookHasMultipleKernels: IContextKey; + private readonly _notebookKernelCount: IContextKey; + private _outputRenderer: OutputRenderer; - protected readonly _contributions: { [key: string]: INotebookEditorContribution; }; + protected readonly _contributions = new Map(); private _scrollBeyondLastLine: boolean; private readonly _memento: Memento; private readonly _activeKernelMemento: Memento; @@ -272,7 +274,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._activeKernelMemento = new Memento(NotebookEditorActiveKernelCache, storageService); this._outputRenderer = new OutputRenderer(this, this.instantiationService); - this._contributions = {}; this._scrollBeyondLastLine = this.configurationService.getValue('editor.scrollBeyondLastLine'); this.configurationService.onDidChangeConfiguration(e => { @@ -289,7 +290,41 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor }); this.notebookService.addNotebookEditor(this); - this._createEditor(); + + const id = generateUuid(); + this._overlayContainer.id = `notebook-${id}`; + this._overlayContainer.className = 'notebookOverlay'; + this._overlayContainer.classList.add('notebook-editor'); + this._overlayContainer.style.visibility = 'hidden'; + + this.layoutService.container.appendChild(this._overlayContainer); + this._createBody(this._overlayContainer); + this._generateFontInfo(); + this._isVisible = true; + this._editorFocus = NOTEBOOK_EDITOR_FOCUSED.bindTo(this.scopedContextKeyService); + this._outputFocus = NOTEBOOK_OUTPUT_FOCUSED.bindTo(this.scopedContextKeyService); + this._editorEditable = NOTEBOOK_EDITOR_EDITABLE.bindTo(this.scopedContextKeyService); + this._editorRunnable = NOTEBOOK_EDITOR_RUNNABLE.bindTo(this.scopedContextKeyService); + this._notebookExecuting = NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK.bindTo(this.scopedContextKeyService); + this._notebookHasMultipleKernels = NOTEBOOK_HAS_MULTIPLE_KERNELS.bindTo(this.scopedContextKeyService); + this._notebookKernelCount = NOTEBOOK_KERNEL_COUNT.bindTo(this.scopedContextKeyService); + + let contributions: INotebookEditorContributionDescription[]; + if (Array.isArray(this.creationOptions.contributions)) { + contributions = this.creationOptions.contributions; + } else { + contributions = NotebookEditorExtensionsRegistry.getEditorContributions(); + } + for (const desc of contributions) { + try { + const contribution = this.instantiationService.createInstance(desc.ctor, this); + this._contributions.set(desc.id, contribution); + } catch (err) { + onUnexpectedError(err); + } + } + + this._updateForNotebookConfiguration(); } /** @@ -359,12 +394,12 @@ 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); - this._editorFocus?.set(focused); + this._editorFocus.set(focused); this.viewModel?.setFocus(focused); } hasFocus() { - return this._editorFocus?.get() || false; + return this._editorFocus.get() || false; } hasWebviewFocus() { @@ -405,46 +440,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return false; } - private _createEditor(): void { - const id = generateUuid(); - this._overlayContainer.id = `notebook-${id}`; - this._overlayContainer.className = 'notebookOverlay'; - this._overlayContainer.classList.add('notebook-editor'); - this._overlayContainer.style.visibility = 'hidden'; - - this.layoutService.container.appendChild(this._overlayContainer); - this._createBody(this._overlayContainer); - this._generateFontInfo(); - this._editorFocus = NOTEBOOK_EDITOR_FOCUSED.bindTo(this.scopedContextKeyService); - this._isVisible = true; - this._outputFocus = NOTEBOOK_OUTPUT_FOCUSED.bindTo(this.scopedContextKeyService); - this._editorEditable = NOTEBOOK_EDITOR_EDITABLE.bindTo(this.scopedContextKeyService); - this._editorEditable.set(true); - this._editorRunnable = NOTEBOOK_EDITOR_RUNNABLE.bindTo(this.scopedContextKeyService); - this._editorRunnable.set(true); - this._notebookExecuting = NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK.bindTo(this.scopedContextKeyService); - this._notebookHasMultipleKernels = NOTEBOOK_HAS_MULTIPLE_KERNELS.bindTo(this.scopedContextKeyService); - this._notebookHasMultipleKernels.set(false); - - let contributions: INotebookEditorContributionDescription[]; - if (Array.isArray(this.creationOptions.contributions)) { - contributions = this.creationOptions.contributions; - } else { - contributions = NotebookEditorExtensionsRegistry.getEditorContributions(); - } - - for (const desc of contributions) { - try { - const contribution = this.instantiationService.createInstance(desc.ctor, this); - this._contributions[desc.id] = contribution; - } catch (err) { - onUnexpectedError(err); - } - } - - this._updateForNotebookConfiguration(); - } - private _generateFontInfo(): void { const editorOptions = this.configurationService.getValue('editor'); this._fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel(), getPixelRatio()); @@ -630,7 +625,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor onWillHide() { this._isVisible = false; - this._editorFocus?.set(false); + this._editorFocus.set(false); this._overlayContainer.style.visibility = 'hidden'; this._overlayContainer.style.left = '-50000px'; } @@ -641,7 +636,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor focus() { this._isVisible = true; - this._editorFocus?.set(true); + this._editorFocus.set(true); if (this._webiewFocused) { this._webview?.focusWebview(); @@ -804,15 +799,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } - if (tokenSource.token.isCancellationRequested) { - return; - } - - if ((availableKernels.length) > 1) { - this._notebookHasMultipleKernels!.set(true); + this._notebookKernelCount.set(availableKernels.length); + if (availableKernels.length > 1) { + this._notebookHasMultipleKernels.set(true); this.multipleKernelsAvailable = true; } else { - this._notebookHasMultipleKernels!.set(false); + this._notebookHasMultipleKernels.set(false); this.multipleKernelsAvailable = false; } @@ -931,12 +923,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } const notebookMetadata = this.viewModel.metadata; - this._editorEditable?.set(!!notebookMetadata?.editable); - this._editorRunnable?.set(this.viewModel.runnable); + this._editorEditable.set(!!notebookMetadata?.editable); + this._editorRunnable.set(this.viewModel.runnable); this._overflowContainer.classList.toggle('notebook-editor-editable', !!notebookMetadata?.editable); this.getDomNode().classList.toggle('notebook-editor-editable', !!notebookMetadata?.editable); - this._notebookExecuting?.set(notebookMetadata.runState === NotebookRunState.Running); + this._notebookExecuting.set(notebookMetadata.runState === NotebookRunState.Running); } private async _resolveWebview(): Promise | null> { @@ -968,7 +960,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } this._webview.webview.onDidBlur(() => { - this._outputFocus?.set(false); + this._outputFocus.set(false); this.updateEditorFocus(); if (this._overlayContainer.contains(document.activeElement)) { @@ -976,7 +968,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } }); this._webview.webview.onDidFocus(() => { - this._outputFocus?.set(true); + this._outputFocus.set(true); this.updateEditorFocus(); this._onDidFocusEmitter.fire(); @@ -1041,10 +1033,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor // contribution state restore const contributionsState = viewState?.contributionsState || {}; - const keys = Object.keys(this._contributions); - for (let i = 0, len = keys.length; i < len; i++) { - const id = keys[i]; - const contribution = this._contributions[id]; + for (const [id, contribution] of this._contributions) { if (typeof contribution.restoreViewState === 'function') { contribution.restoreViewState(contributionsState[id]); } @@ -1219,10 +1208,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor // Save contribution view states const contributionsState: { [key: string]: unknown } = {}; - - const keys = Object.keys(this._contributions); - for (const id of keys) { - const contribution = this._contributions[id]; + for (const [id, contribution] of this._contributions) { if (typeof contribution.saveViewState === 'function') { contributionsState[id] = contribution.saveViewState(); } @@ -2146,8 +2132,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor //#endregion //#region Editor Contributions - public getContribution(id: string): T { - return (this._contributions[id] || null); + getContribution(id: string): T { + return (this._contributions.get(id) || null); } //#endregion @@ -2159,11 +2145,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._webview?.dispose(); this.notebookService.removeNotebookEditor(this); - const keys = Object.keys(this._contributions); - for (let i = 0, len = keys.length; i < len; i++) { - const contributionId = keys[i]; - this._contributions[contributionId].dispose(); - } + dispose(this._contributions.values()); + this._contributions.clear(); this._localStore.clear(); this._list.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 3e6484a7baa..3aabfc323aa 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -680,6 +680,9 @@ export class NotebookService extends Disposable implements INotebookService, ICu } registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: IMainNotebookController): IDisposable { + if (this._notebookProviders.has(viewType)) { + throw new Error(`notebook controller for viewtype '${viewType}' already exists`); + } this._notebookProviders.set(viewType, { extensionData, controller }); if (controller.viewOptions && !this.notebookProviderInfoStore.get(viewType)) { 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 05567647704..6eff616e86e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -199,7 +199,8 @@ abstract class AbstractCellRenderer { } return undefined; - } + }, + respectOrientationForPreviousAndNextKey: true }); const cellMenu = this.instantiationService.createInstance(CellMenus); @@ -233,7 +234,8 @@ abstract class AbstractCellRenderer { } return createActionViewItem(this.instantiationService, action); }, - renderDropdownAsChildElement: true + renderDropdownAsChildElement: true, + respectOrientationForPreviousAndNextKey: true }); if (elementClass) { @@ -1108,7 +1110,8 @@ export class ListTopCellToolbar extends Disposable { } return undefined; - } + }, + respectOrientationForPreviousAndNextKey: true }); const cellMenu = this.instantiationService.createInstance(CellMenus); 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 03a42b55550..b45001f687b 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -308,9 +308,8 @@ export class StatefulMarkdownCell extends Disposable { this.templateData.editorContainer.innerText = ''; // create a special context key service that set the inCompositeEditor-contextkey - const editorContextKeyService = this.contextKeyService.createOverlay([ - [EditorContextKeys.inCompositeEditor.key, true] - ]); + const editorContextKeyService = this.contextKeyService.createScoped(this.templateData.editorPart); + EditorContextKeys.inCompositeEditor.bindTo(editorContextKeyService).set(true); const editorInstaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, editorContextKeyService])); this.editor = editorInstaService.createInstance(CodeEditorWidget, this.templateData.editorContainer, { diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 1ad9b60a8b6..583dad5a8cf 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -125,6 +125,7 @@ export class NotebookEditorModel extends EditorModel implements INotebookEditorM async revert(options?: IRevertOptions | undefined): Promise { if (options?.soft) { await this._backupFileService.discardBackup(this.resource); + this.setDirty(false); return; } diff --git a/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/src/vs/workbench/contrib/outline/browser/outlinePane.ts index 5a3c4048178..e988a1a3a9d 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePane.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePane.ts @@ -231,7 +231,7 @@ export class OutlinePane extends ViewPane { { ...newOutline.config.options, sorter, - openOnSingleClick: true, + expandOnDoubleClick: false, expandOnlyOnTwistieClick: true, multipleSelectionSupport: false, hideTwistiesOfChildlessElements: true, diff --git a/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts b/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts index 93974d117c3..51d3d495873 100644 --- a/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts +++ b/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts @@ -174,6 +174,7 @@ class PerfModelContentProvider implements ITextModelContentProvider { table.push(['window.loadUrl() => begin to require(workbench.desktop.main.js)', metrics.timers.ellapsedWindowLoadToRequire, '[main->renderer]', StartupKindToString(metrics.windowKind)]); table.push(['require(workbench.desktop.main.js)', metrics.timers.ellapsedRequire, '[renderer]', `cached data: ${(metrics.didUseCachedData ? 'YES' : 'NO')}${stats ? `, node_modules took ${stats.nodeRequireTotal}ms` : ''}`]); table.push(['wait for shell environment', metrics.timers.ellapsedWaitForShellEnv, '[renderer]', undefined]); + table.push(['init storage (global & workspace)', metrics.timers.ellapsedStorageInit, '[renderer]', undefined]); table.push(['require & init workspace storage', metrics.timers.ellapsedWorkspaceStorageInit, '[renderer]', undefined]); table.push(['init workspace service', metrics.timers.ellapsedWorkspaceServiceInit, '[renderer]', undefined]); if (isWeb) { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 6cee6eb3ffe..10b63e2e4ec 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -651,6 +651,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const toolbar = new ToolBar(container, this._contextMenuService, { toggleMenuTitle, renderDropdownAsChildElement: true, + respectOrientationForPreviousAndNextKey: true, moreIcon: settingsMoreActionIcon // change icon from ellipsis to gear }); return toolbar; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index 9994ccc1b5e..aea2722c343 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -374,7 +374,7 @@ abstract class AbstractListSettingWidget extends Dispo rowElement.setAttribute('tabindex', item.selected ? '0' : '-1'); rowElement.classList.toggle('selected', item.selected); - const actionBar = new ActionBar(rowElement); + const actionBar = new ActionBar(rowElement, { respectOrientationForPreviousAndNextKey: true }); this.listDisposables.add(actionBar); actionBar.push(this.getActionsForItem(item, idx), { icon: true, label: true }); diff --git a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts index f3ba2a45148..89a5e8545f4 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -219,10 +219,13 @@ export class AutomaticPortForwarding extends Disposable implements IWorkbenchCon this._register(new OutputAutomaticPortForwarding(terminalService, notificationService, openerService, externalOpenerService, remoteExplorerService, configurationService, debugService, tunnelService, remoteAgentService, hostService, false)); } else if (environment?.os === OperatingSystem.Linux) { - this._register(new ProcAutomaticPortForwarding(configurationService, remoteExplorerService, notificationService, - openerService, externalOpenerService, tunnelService, hostService)); + const useProc = (this.configurationService.getValue('remote.autoForwardPortsSource') === 'process'); + if (useProc) { + this._register(new ProcAutomaticPortForwarding(configurationService, remoteExplorerService, notificationService, + openerService, externalOpenerService, tunnelService, hostService)); + } this._register(new OutputAutomaticPortForwarding(terminalService, notificationService, openerService, externalOpenerService, - remoteExplorerService, configurationService, debugService, tunnelService, remoteAgentService, hostService, true)); + remoteExplorerService, configurationService, debugService, tunnelService, remoteAgentService, hostService, useProc)); } }); } @@ -435,7 +438,7 @@ class OutputAutomaticPortForwarding extends Disposable { } this.urlFinder = this._register(new UrlFinder(this.terminalService, this.debugService)); this._register(this.urlFinder.onDidMatchLocalUrl(async (localUrl) => { - if (mapHasAddressLocalhostOrAllInterfaces(this.remoteExplorerService.tunnelModel.forwarded, localUrl.host, localUrl.port)) { + if (mapHasAddressLocalhostOrAllInterfaces(this.remoteExplorerService.tunnelModel.detected, localUrl.host, localUrl.port)) { return; } if (this.portsAttributes.getAttributes(localUrl.port)?.onAutoForward === OnPortForward.Ignore) { @@ -544,6 +547,9 @@ class ProcAutomaticPortForwarding extends Disposable { if (this.initialCandidates.has(address)) { return undefined; } + if (this.notifiedOnly.has(address) || this.autoForwarded.has(address)) { + return undefined; + } const alreadyForwarded = mapHasAddressLocalhostOrAllInterfaces(this.remoteExplorerService.tunnelModel.forwarded, value.host, value.port); if (mapHasAddressLocalhostOrAllInterfaces(this.remoteExplorerService.tunnelModel.detected, value.host, value.port)) { return undefined; diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index daa3fba23f9..02b75df99e9 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -225,7 +225,8 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer 0) { urlMatches.forEach((match) => { // check if valid url - const serverUrl = new URL(match); + let serverUrl; + try { + serverUrl = new URL(match); + } catch (e) { + // Not a valid URL + } if (serverUrl) { // check if the port is a valid integer value const portMatch = match.match(UrlFinder.extractPortRegex); diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index 043cd4876ad..395e5c7cce5 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -134,6 +134,16 @@ Registry.as(ConfigurationExtensions.Configuration) markdownDescription: localize('remote.autoForwardPorts', "When enabled, new running processes are detected and ports that they listen on are automatically forwarded."), default: true }, + 'remote.autoForwardPortsSource': { + type: 'string', + markdownDescription: localize('remote.autoForwardPortsSource', "Sets the source from which ports are automatically forwarded when `remote.autoForwardPorts` is true. Requires a reload to take effect."), + enum: ['process', 'output'], + enumDescriptions: [ + localize('remote.autoForwardPortsSource.process', "Ports will be automatically forwarded when discovered by watching for processes that are started and include a port."), + localize('remote.autoForwardPortsSource.output', "Ports will be automatically forwarded when discovered by reading terminal and debug output. Not all processes that use ports will print to the integrated terminal or debug console, so some ports will be missed. Ports forwarded based on output will not be \"un-forwarded\" until reload or until the port is closed by the user in the Ports view.") + ], + default: 'process' + }, // Consider making changes to extensions\configuration-editing\schemas\devContainer.schema.src.json // and extensions\configuration-editing\schemas\attachContainer.schema.json // to keep in sync with devcontainer.json schema. diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts index ffcb36be97f..803d80ee982 100644 --- a/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts @@ -58,7 +58,7 @@ export class RepositoryRenderer implements ICompressibleTreeRenderer { + this._register(results.onTestChanged(({ item: result }) => { for (const i of this.items.values()) { - if (i.test.item.extId === item.extId) { - i.ownState = state.state; - i.retired = retired; - refreshComputedState(computedStateAccessor, i, this.addUpdated, computedState); + if (i.test.item.extId === result.item.extId) { + i.ownState = result.state.state; + i.retired = result.retired; + refreshComputedState(computedStateAccessor, i, this.addUpdated, result.computedState); this.addUpdated(i); this.updateEmitter.fire(); return; diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/locationStore.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/locationStore.ts index 9e23c907a81..f604a164eec 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/locationStore.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/locationStore.ts @@ -6,25 +6,22 @@ import { findFirstInSorted } from 'vs/base/common/arrays'; import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; -import { Location as ModeLocation } from 'vs/editor/common/modes'; +import { Range } from 'vs/editor/common/core/range'; +import { IRichLocation } from 'vs/workbench/contrib/testing/common/testCollection'; -export const locationsEqual = (a: ModeLocation | undefined, b: ModeLocation | undefined) => { +export const locationsEqual = (a: IRichLocation | undefined, b: IRichLocation | undefined) => { if (a === undefined || b === undefined) { return b === a; } - return a.uri.toString() === b.uri.toString() - && a.range.startLineNumber === b.range.startLineNumber - && a.range.startColumn === b.range.startColumn - && a.range.endLineNumber === b.range.endLineNumber - && a.range.endColumn === b.range.endColumn; + return a.uri.toString() === b.uri.toString() && a.range.equalsRange(b.range); }; /** * Stores and looks up test-item-like-objects by their uri/range. Used to * implement the 'reveal' action efficiently. */ -export class TestLocationStore { +export class TestLocationStore { private readonly itemsByUri = new Map(); public hasTestInDocument(uri: URI) { @@ -39,12 +36,7 @@ export class TestLocationStore { const range = test.location?.range; - return range - && new Position(range.startLineNumber, range.startColumn).isBeforeOrEqual(position) - && position.isBeforeOrEqual(new Position( - range.endLineNumber ?? range.startLineNumber, - range.endColumn ?? range.startColumn, - )); + return range && Range.lift(range).containsPosition(position); }); } diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index 60ffcfc38c0..2dac0056fb6 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -26,8 +26,9 @@ import { InternalTestItem, TestIdWithProvider } from 'vs/workbench/contrib/testi import { ITestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestResult, ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { ITestService, waitForAllRoots } from 'vs/workbench/contrib/testing/common/testService'; +import { ITestService, waitForAllRoots, waitForAllTests } from 'vs/workbench/contrib/testing/common/testService'; import { IWorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; const category = localize('testing.category', 'Test'); @@ -94,7 +95,7 @@ export class RunAction extends Action { } } -abstract class RunOrDebugAction extends ViewAction { +abstract class RunOrDebugSelectedAction extends ViewAction { constructor(id: string, title: string, icon: ThemeIcon, private readonly debug: boolean) { super({ id, @@ -103,6 +104,7 @@ abstract class RunOrDebugAction extends ViewAction { viewId: Testing.ExplorerViewId, f1: true, category, + precondition: FocusedViewContext.isEqualTo(Testing.ExplorerViewId), }); } @@ -143,7 +145,7 @@ abstract class RunOrDebugAction extends ViewAction { protected abstract filter(item: InternalTestItem): boolean; } -export class RunSelectedAction extends RunOrDebugAction { +export class RunSelectedAction extends RunOrDebugSelectedAction { constructor( ) { super( @@ -162,7 +164,7 @@ export class RunSelectedAction extends RunOrDebugAction { } } -export class DebugSelectedAction extends RunOrDebugAction { +export class DebugSelectedAction extends RunOrDebugSelectedAction { constructor() { super( 'testing.debugSelected', @@ -502,3 +504,166 @@ export class ToggleAutoRun extends Action2 { accessor.get(ITestingAutoRun).toggle(); } } + +abstract class RunOrDebugAtCursor extends Action2 { + /** + * @override + */ + public async run(accessor: ServicesAccessor) { + const control = accessor.get(IEditorService).activeTextEditorControl; + const position = control?.getPosition(); + const model = control?.getModel(); + if (!position || !model || !('uri' in model)) { + return; + } + + + const testService = accessor.get(ITestService); + const collection = testService.subscribeToDiffs(ExtHostTestingResource.TextDocument, model.uri); + + let bestDepth = -1; + let bestNode: InternalTestItem | undefined; + + try { + await waitForAllTests(collection.object); + const queue: [depth: number, nodes: Iterable][] = [[0, collection.object.rootIds]]; + while (queue.length > 0) { + const [depth, candidates] = queue.pop()!; + for (const id of candidates) { + const candidate = collection.object.getNodeById(id); + if (candidate) { + if (depth > bestDepth && this.filter(candidate) && candidate.item.location?.range.containsPosition(position)) { + bestDepth = depth; + bestNode = candidate; + } + + queue.push([depth + 1, candidate.children]); + } + } + } + + if (bestNode) { + await this.runTest(testService, bestNode); + } + } finally { + collection.dispose(); + } + } + + protected abstract filter(node: InternalTestItem): boolean; + + protected abstract runTest(service: ITestService, node: InternalTestItem): Promise; +} + +export class RunAtCursor extends RunOrDebugAtCursor { + constructor() { + super({ + id: 'testing.runAtCursor', + title: localize('testing.runAtCursor', "Run Test at Cursor"), + f1: true, + category, + }); + } + + protected filter(node: InternalTestItem): boolean { + return node.item.runnable; + } + + protected runTest(service: ITestService, node: InternalTestItem): Promise { + return service.runTests({ debug: false, tests: [{ testId: node.id, providerId: node.providerId }] }); + } +} + +export class DebugAtCursor extends RunOrDebugAtCursor { + constructor() { + super({ + id: 'testing.debugAtCursor', + title: localize('testing.debugAtCursor', "Debug Test at Cursor"), + f1: true, + category, + }); + } + + protected filter(node: InternalTestItem): boolean { + return node.item.debuggable; + } + + protected runTest(service: ITestService, node: InternalTestItem): Promise { + return service.runTests({ debug: true, tests: [{ testId: node.id, providerId: node.providerId }] }); + } +} + + +abstract class RunOrDebugCurrentFile extends Action2 { + /** + * @override + */ + public async run(accessor: ServicesAccessor) { + const control = accessor.get(IEditorService).activeTextEditorControl; + const position = control?.getPosition(); + const model = control?.getModel(); + if (!position || !model || !('uri' in model)) { + return; + } + + const testService = accessor.get(ITestService); + const collection = testService.subscribeToDiffs(ExtHostTestingResource.TextDocument, model.uri); + + try { + await waitForAllTests(collection.object); + + const roots = [...collection.object.rootIds] + .map(r => collection.object.getNodeById(r)) + .filter(isDefined) + .filter(n => this.filter(n)); + + if (roots.length) { + await this.runTest(testService, roots); + } + } finally { + collection.dispose(); + } + } + + protected abstract filter(node: InternalTestItem): boolean; + + protected abstract runTest(service: ITestService, node: InternalTestItem[]): Promise; +} + +export class RunCurrentFile extends RunOrDebugCurrentFile { + constructor() { + super({ + id: 'testing.runCurrentFile', + title: localize('testing.runCurrentFile', "Run Tests in Current File"), + f1: true, + category, + }); + } + + protected filter(node: InternalTestItem): boolean { + return node.item.runnable; + } + + protected runTest(service: ITestService, nodes: InternalTestItem[]): Promise { + return service.runTests({ debug: false, tests: nodes.map(node => ({ testId: node.id, providerId: node.providerId })) }); + } +} + +export class DebugCurrentFile extends RunOrDebugCurrentFile { + constructor() { + super({ + id: 'testing.debugCurrentFile', + title: localize('testing.debugCurrentFile', "Debug Tests in Current File"), + f1: true, + category, + }); + } + + protected filter(node: InternalTestItem): boolean { + return node.item.debuggable; + } + + protected runTest(service: ITestService, nodes: InternalTestItem[]): Promise { + return service.runTests({ debug: true, tests: nodes.map(node => ({ testId: node.id, providerId: node.providerId })) }); + } +} diff --git a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts index 3cbf194564e..6ef650fcedf 100644 --- a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts +++ b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts @@ -19,7 +19,7 @@ import { testingViewIcon } from 'vs/workbench/contrib/testing/browser/icons'; import { TestingDecorations } from 'vs/workbench/contrib/testing/browser/testingDecorations'; import { ITestExplorerFilterState, TestExplorerFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; import { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView'; -import { CloseTestPeek, TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; +import { CloseTestPeek, ITestingPeekOpener, TestingOutputPeekController, TestingPeekOpener } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer'; import { testingConfiguation } from 'vs/workbench/contrib/testing/common/configuration'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; @@ -38,6 +38,7 @@ registerSingleton(ITestService, TestService); registerSingleton(ITestResultService, TestResultService); registerSingleton(ITestExplorerFilterState, TestExplorerFilterState); registerSingleton(ITestingAutoRun, TestingAutoRun, true); +registerSingleton(ITestingPeekOpener, TestingPeekOpener); registerSingleton(IWorkspaceTestCollectionService, WorkspaceTestCollectionService); const viewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ @@ -96,6 +97,10 @@ registerAction2(Action.DebugAllAction); registerAction2(Action.EditFocusedTest); registerAction2(Action.ClearTestResultsAction); registerAction2(Action.ToggleAutoRun); +registerAction2(Action.DebugAtCursor); +registerAction2(Action.RunAtCursor); +registerAction2(Action.DebugCurrentFile); +registerAction2(Action.RunCurrentFile); registerAction2(CloseTestPeek); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Eventually); @@ -127,4 +132,14 @@ CommandsRegistry.registerCommand({ } }); +CommandsRegistry.registerCommand({ + id: 'vscode.peekTestError', + handler: async (accessor: ServicesAccessor, extId: string) => { + const lookup = accessor.get(ITestResultService).getStateByExtId(extId); + if (lookup) { + accessor.get(ITestingPeekOpener).tryPeekFirstError(lookup[0], lookup[1]); + } + } +}); + Registry.as(ConfigurationExtensions.Configuration).registerConfiguration(testingConfiguation); diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index 01d809934ca..567258a3597 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -14,7 +14,6 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IRange } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; -import { Location as ModeLocation } from 'vs/editor/common/modes'; import { overviewRulerError, overviewRulerInfo, overviewRulerWarning } from 'vs/editor/common/view/editorColorRegistry'; import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -27,9 +26,9 @@ import { BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution } from import { testingRunAllIcon, testingRunIcon, testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons'; import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; import { testMessageSeverityColors } from 'vs/workbench/contrib/testing/browser/theme'; -import { IncrementalTestCollectionItem, ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection'; +import { IncrementalTestCollectionItem, IRichLocation, ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection'; import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri'; -import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; +import { ITestResultService, TestResultItem } from 'vs/workbench/contrib/testing/common/testResultService'; import { IMainThreadTestCollection, ITestService } from 'vs/workbench/contrib/testing/common/testService'; export class TestingDecorations extends Disposable implements IEditorContribution { @@ -63,8 +62,8 @@ export class TestingDecorations extends Disposable implements IEditorContributio } this.collection.value = this.testService.subscribeToDiffs(ExtHostTestingResource.TextDocument, uri, () => this.setDecorations(uri)); - this._register(this.results.onTestChanged(([, changed]) => { - if (changed.item.location?.uri.toString() === uri.toString()) { + this._register(this.results.onTestChanged(({ item: result }) => { + if (result.item.location?.uri.toString() === uri.toString()) { this.setDecorations(uri); } })); @@ -87,7 +86,7 @@ export class TestingDecorations extends Disposable implements IEditorContributio const stateLookup = this.results.getStateByExtId(test.item.extId); if (hasValidLocation(uri, test.item)) { newDecorations.push(this.instantiationService.createInstance( - RunTestDecoration, test, ref.object, test.item.location, this.editor, stateLookup?.[1].computedState)); + RunTestDecoration, test, ref.object, test.item.location, this.editor, stateLookup?.[1])); } if (!stateLookup) { @@ -102,7 +101,7 @@ export class TestingDecorations extends Disposable implements IEditorContributio type: TestUriType.ResultActualOutput, messageIndex: i, resultId: result.id, - testId: stateItem.item.extId, + testExtId: stateItem.item.extId, }); newDecorations.push(this.instantiationService.createInstance(TestMessageDecoration, m, uri, m.location, this.editor)); @@ -144,7 +143,7 @@ interface ITestDecoration extends IDisposable { click(e: IEditorMouseEvent): boolean; } -const hasValidLocation = (editorUri: URI, t: T): t is T & { location: ModeLocation } => +const hasValidLocation = (editorUri: URI, t: T): t is T & { location: IRichLocation } => t.location?.uri.toString() === editorUri.toString(); const firstLineRange = (originalRange: IRange) => ({ @@ -170,9 +169,9 @@ class RunTestDecoration extends Disposable implements ITestDecoration { constructor( private readonly test: IncrementalTestCollectionItem, private readonly collection: IMainThreadTestCollection, - private readonly location: ModeLocation, + private readonly location: IRichLocation, private readonly editor: ICodeEditor, - computedState: TestRunState | undefined, + stateItem: TestResultItem | undefined, @ITestService private readonly testService: ITestService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @ICommandService private readonly commandService: ICommandService, @@ -180,14 +179,21 @@ class RunTestDecoration extends Disposable implements ITestDecoration { super(); this.line = location.range.startLineNumber; - const icon = computedState !== undefined && computedState !== TestRunState.Unset - ? testingStatesToIcons.get(computedState)! + const icon = stateItem?.computedState !== undefined && stateItem.computedState !== TestRunState.Unset + ? testingStatesToIcons.get(stateItem.computedState)! : test.children.size > 0 ? testingRunAllIcon : testingRunIcon; + const hoverMessage = new MarkdownString('', true).appendText(localize('failedHoverMessage', '{0} has failed. ', test.item.label)); + if (stateItem?.state.messages.length) { + const args = encodeURIComponent(JSON.stringify([test.item.extId])); + hoverMessage.appendMarkdown(`[${localize('failedPeekAction', 'Peek Error')}](command:vscode.peekTestError?${args})`); + } + this.editorDecoration = { range: firstLineRange(this.location.range), options: { isWholeLine: true, + hoverMessage, glyphMarginClassName: ThemeIcon.asClassName(icon) + ' testing-run-glyph', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, glyphMarginHoverMessage: new MarkdownString().appendText(localize('testing.clickToRun', 'Click to run tests, right click for more options')), @@ -275,7 +281,7 @@ class TestMessageDecoration implements ITestDecoration { constructor( { message, severity }: ITestMessage, private readonly messageUri: URI, - location: ModeLocation, + location: IRichLocation, private readonly editor: ICodeEditor, @ICodeEditorService private readonly editorService: ICodeEditorService, @IThemeService themeService: IThemeService, diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts index eae8ec3d55d..3c102a91fd5 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts @@ -9,7 +9,7 @@ import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; import { HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox'; -import { Action, IAction, IActionRunner } from 'vs/base/common/actions'; +import { Action, IAction, IActionRunner, Separator } from 'vs/base/common/actions'; import { Delayer } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -24,7 +24,7 @@ import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ViewContainerLocation } from 'vs/workbench/common/views'; import { testingFilterIcon } from 'vs/workbench/contrib/testing/browser/icons'; -import { Testing } from 'vs/workbench/contrib/testing/common/constants'; +import { TestExplorerStateFilter, Testing } from 'vs/workbench/contrib/testing/common/constants'; import { ObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; @@ -34,6 +34,7 @@ export interface ITestExplorerFilterState { readonly text: ObservableValue; /** Reveal request, the extId of the test to reveal */ readonly reveal: ObservableValue; + readonly stateFilter: ObservableValue; readonly currentDocumentOnly: ObservableValue; readonly onDidRequestInputFocus: Event; @@ -46,6 +47,11 @@ export class TestExplorerFilterState implements ITestExplorerFilterState { declare _serviceBrand: undefined; private readonly focusEmitter = new Emitter(); public readonly text = new ObservableValue(''); + public readonly stateFilter = ObservableValue.stored(new StoredValue({ + key: 'testStateFilter', + scope: StorageScope.WORKSPACE, + target: StorageTarget.USER + }, this.storage), TestExplorerStateFilter.All); public readonly currentDocumentOnly = ObservableValue.stored(new StoredValue({ key: 'testsByCurrentDocumentOnly', scope: StorageScope.WORKSPACE, @@ -83,6 +89,7 @@ export class TestingExplorerFilter extends BaseActionViewItem { super(null, action); this.updateFilterActiveState(); this._register(state.currentDocumentOnly.onDidChange(this.updateFilterActiveState, this)); + this._register(state.stateFilter.onDidChange(this.updateFilterActiveState, this)); } /** @@ -166,7 +173,8 @@ export class TestingExplorerFilter extends BaseActionViewItem { * Updates the 'checked' state of the filter submenu. */ private updateFilterActiveState() { - this.filtersAction.checked = this.state.currentDocumentOnly.value; + this.filtersAction.checked = this.state.currentDocumentOnly.value + || this.state.stateFilter.value !== TestExplorerStateFilter.All; } } @@ -198,6 +206,21 @@ class FiltersDropdownMenuActionViewItem extends DropdownMenuActionViewItem { private getActions(): IAction[] { return [ + ...[ + { v: TestExplorerStateFilter.OnlyFailed, label: localize('testing.filters.showOnlyFailed', "Show Only Failed Tests") }, + { v: TestExplorerStateFilter.OnlyExecuted, label: localize('testing.filters.showOnlyExecuted', "Show Only Executed Tests") }, + { v: TestExplorerStateFilter.All, label: localize('testing.filters.showAll', "Show All Tests") }, + ].map(({ v, label }) => ({ + checked: this.filters.stateFilter.value === v, + class: undefined, + enabled: true, + id: v, + label, + run: async () => this.filters.stateFilter.value = v, + tooltip: '', + dispose: () => null + })), + new Separator(), { checked: this.filters.currentDocumentOnly.value, class: undefined, diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index b1a3c35be43..bcbd917f11f 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -50,11 +50,10 @@ import { HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/b import { HierarchicalByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName'; import { testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons'; import { ITestExplorerFilterState, TestExplorerFilterState, TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; -import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; -import { TestExplorerViewMode, TestExplorerViewSorting, Testing, testStateNames } from 'vs/workbench/contrib/testing/common/constants'; +import { ITestingPeekOpener, TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; +import { TestExplorerStateFilter, TestExplorerViewMode, TestExplorerViewSorting, Testing, testStateNames } from 'vs/workbench/contrib/testing/common/constants'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { cmpPriority, isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; -import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri'; import { ITestResultService, sumCounts, TestStateCount } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { IWorkspaceTestCollectionService, TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; @@ -230,6 +229,7 @@ export class TestingExplorerViewModel extends Disposable { @IStorageService private readonly storageService: IStorageService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @ITestResultService private readonly testResults: ITestResultService, + @ITestingPeekOpener private readonly peekOpener: ITestingPeekOpener, ) { super(); @@ -257,7 +257,11 @@ export class TestingExplorerViewModel extends Disposable { filter: this.filter, }) as WorkbenchObjectTree; - this._register(Event.any(filterState.currentDocumentOnly.onDidChange, filterState.text.onDidChange)(this.tree.refilter, this.tree)); + this._register(Event.any( + filterState.currentDocumentOnly.onDidChange, + filterState.text.onDidChange, + filterState.stateFilter.onDidChange, + )(this.tree.refilter, this.tree)); this._register(editorService.onDidActiveEditorChange(() => { if (filterState.currentDocumentOnly.value && editorService.activeEditor?.resource) { if (this.projection.hasTestInDocument(editorService.activeEditor.resource)) { @@ -386,35 +390,9 @@ export class TestingExplorerViewModel extends Disposable { */ private async tryPeekError(item: ITestTreeElement) { const lookup = item.test && this.testResults.getStateByExtId(item.test.item.extId); - if (!lookup || !isFailedState(lookup[1].state.state)) { - return false; - } - - const [result, test] = lookup; - const index = test.state.messages.findIndex(m => !!m.location); - if (index === -1) { - return; - } - - const message = test.state.messages[index]; - const pane = await this.editorService.openEditor({ - resource: message.location!.uri, - options: { selection: message.location!.range, preserveFocus: true } - }); - - const control = pane?.getControl(); - if (!isCodeEditor(control)) { - return false; - } - - TestingOutputPeekController.get(control).show(buildTestUri({ - type: TestUriType.ResultMessage, - messageIndex: index, - resultId: result.id, - testId: item.test!.item.extId, - })); - - return true; + return lookup && isFailedState(lookup[1].state.state) + ? this.peekOpener.tryPeekFirstError(lookup[0], lookup[1], { preserveFocus: true }) + : false; } private updatePreferredProjection() { @@ -549,7 +527,7 @@ class TestsFilter implements ITreeFilter { this.setFilter(this.state.text.value); } - switch (Math.min(this.testFilterText(element), this.testLocation(element))) { + switch (Math.min(this.testFilterText(element), this.testLocation(element), this.testState(element))) { case FilterResult.Exclude: return TreeVisibility.Hidden; case FilterResult.Include: @@ -559,6 +537,17 @@ class TestsFilter implements ITreeFilter { } } + private testState(element: ITestTreeElement): FilterResult { + switch (this.state.stateFilter.value) { + case TestExplorerStateFilter.All: + return FilterResult.Include; + case TestExplorerStateFilter.OnlyExecuted: + return element.ownState !== TestRunState.Unset ? FilterResult.Include : FilterResult.Inherit; + case TestExplorerStateFilter.OnlyFailed: + return isFailedState(element.ownState) ? FilterResult.Include : FilterResult.Inherit; + } + } + private testLocation(element: ITestTreeElement): FilterResult { if (!this._filterToUri || !this.state.currentDocumentOnly.value) { return FilterResult.Include; @@ -700,7 +689,8 @@ class TestsRenderer implements ITreeRenderer action instanceof MenuItemAction ? this.instantiationService.createInstance(MenuEntryActionViewItem, action) - : undefined + : undefined, + respectOrientationForPreviousAndNextKey: true }); return { label, actionBar, icon }; diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 081c8893ad9..3ee275e166a 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -11,7 +11,7 @@ import { Disposable, IReference, MutableDisposable } from 'vs/base/common/lifecy import { clamp } from 'vs/base/common/numbers'; import { count } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction2 } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; @@ -20,17 +20,22 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { getOuterEditor, IPeekViewService, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground, PeekViewWidget } from 'vs/editor/contrib/peekView/peekView'; import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { createDecorator, IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { EditorModel } from 'vs/workbench/common/editor'; import { testingPeekBorder } from 'vs/workbench/contrib/testing/browser/theme'; +import { AutoOpenPeekViewWhen, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; import { ITestItem, ITestMessage, ITestState } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; +import { isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; import { buildTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri'; -import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; +import { ITestResult, ITestResultService, TestResultItem, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResultService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; interface ITestDto { test: ITestItem, @@ -41,6 +46,93 @@ interface ITestDto { messageUri: URI; } +export interface ITestingPeekOpener { + _serviceBrand: undefined; + + /** + * Tries to peek the first test error, if the item is in a failed state. + * @returns a boolean indicating whether a peek was opened + */ + tryPeekFirstError(result: ITestResult, test: TestResultItem, options?: Partial): Promise; +} + +export const ITestingPeekOpener = createDecorator('testingPeekOpener'); + +export class TestingPeekOpener extends Disposable implements ITestingPeekOpener { + declare _serviceBrand: undefined; + + constructor( + @IConfigurationService private readonly configuration: IConfigurationService, + @IEditorService private readonly editorService: IEditorService, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService, + @ITestResultService testResults: ITestResultService, + ) { + super(); + this._register(testResults.onTestChanged(this.openPeekOnFailure, this)); + } + + /** + * Tries to peek the first test error, if the item is in a failed state. + * @returns a boolean if a peek was opened + */ + public async tryPeekFirstError(result: ITestResult, test: TestResultItem, options?: Partial) { + const index = test.state.messages.findIndex(m => !!m.location); + if (index === -1) { + return false; + } + + const message = test.state.messages[index]; + const pane = await this.editorService.openEditor({ + resource: message.location!.uri, + options: { selection: message.location!.range, revealIfOpened: true, ...options } + }); + + const control = pane?.getControl(); + if (!isCodeEditor(control)) { + return false; + } + + TestingOutputPeekController.get(control).show(buildTestUri({ + type: TestUriType.ResultMessage, + messageIndex: index, + resultId: result.id, + testExtId: test.item.extId, + })); + + return true; + } + + /** + * Opens the peek view on a test failure, based on user preferences. + */ + private openPeekOnFailure(evt: TestResultItemChange) { + if (!isFailedState(evt.item.state.state) || !evt.item.state.messages.length) { + return; + } + + if (evt.result.isAutoRun && !getTestingConfiguration(this.configuration, TestingConfigKeys.AutoOpenPeekViewDuringAutoRun)) { + return; + } + + const editors = this.codeEditorService.listCodeEditors(); + const cfg = getTestingConfiguration(this.configuration, TestingConfigKeys.AutoOpenPeekView); + + // don't show the peek if the user asked to only auto-open peeks for visible tests, + // and this test is not in any of the editors' models. + const testUri = evt.item.item.location?.uri.toString(); + if (cfg === AutoOpenPeekViewWhen.FailureVisible && (!testUri || !editors.some(e => e.getModel()?.uri.toString() === testUri))) { + return; + } + + const controllers = editors.map(TestingOutputPeekController.get); + if (controllers.some(c => c?.isVisible)) { + return; + } + + this.tryPeekFirstError(evt.result, evt.item); + } +} + /** * Adds output/message peek functionality to code editors. */ @@ -62,6 +154,13 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo */ private readonly visible: IContextKey; + /** + * Gets whether a peek is currently shown in the associated editor. + */ + public get isVisible() { + return this.peek.value; + } + constructor( private readonly editor: ICodeEditor, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -71,6 +170,7 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo super(); this.visible = TestingContextKeys.isPeekVisible.bindTo(contextKeyService); this._register(editor.onDidChangeModel(() => this.peek.clear())); + this._register(testResults.onTestChanged((evt) => this.closePeekOnTestChange(evt))); } /** @@ -113,13 +213,28 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo this.peek.clear(); } - private async retrieveTest(uri: URI): Promise { + /** + * If the test we're currently showing has its state change to something + * else, then clear the peek. + */ + private closePeekOnTestChange(evt: TestResultItemChange) { + if (evt.reason !== TestResultItemChangeReason.OwnStateChange || evt.previous.state === evt.item.state.state) { + return; + } + + const displayed = this.peek.value?.currentTest(); + if (displayed?.extId === evt.item.item.extId) { + this.peek.clear(); + } + } + + private retrieveTest(uri: URI): ITestDto | undefined { const parts = parseTestUri(uri); if (!parts) { return undefined; } - const test = this.testResults.getResult(parts.resultId)?.getStateByExtId(parts.testId); + const test = this.testResults.getResult(parts.resultId)?.getStateByExtId(parts.testExtId); return test && { test: test.item, state: test.state, @@ -168,6 +283,11 @@ abstract class TestingOutputPeek extends PeekViewWidget { */ public abstract setModel(dto: ITestDto): Promise; + /** + * Returns the test whose data is currently shown in the peek view. + */ + public abstract currentTest(): ITestItem | undefined; + /** * @override */ @@ -205,6 +325,7 @@ const diffEditorOptions: IDiffEditorOptions = { class TestingDiffOutputPeek extends TestingOutputPeek { private readonly diff = this._disposables.add(new MutableDisposable()); + private test: ITestItem | undefined; /** * @override @@ -227,6 +348,7 @@ class TestingDiffOutputPeek extends TestingOutputPeek { return; } + this.test = test; this.show(message.location.range, hintDiffPeekHeight(message)); this.setTitle(message.message.toString(), test.label); @@ -243,6 +365,13 @@ class TestingDiffOutputPeek extends TestingOutputPeek { } } + /** + * @override + */ + public currentTest() { + return this.test; + } + /** * @override */ @@ -254,6 +383,7 @@ class TestingDiffOutputPeek extends TestingOutputPeek { class TestingMessageOutputPeek extends TestingOutputPeek { private readonly preview = this._disposables.add(new MutableDisposable()); + private test: ITestItem | undefined; /** * @override @@ -276,6 +406,7 @@ class TestingMessageOutputPeek extends TestingOutputPeek { return; } + this.test = test; this.show(message.location.range, hintPeekStrHeight(message.message.toString())); this.setTitle(message.message.toString(), test.label); @@ -287,6 +418,13 @@ class TestingMessageOutputPeek extends TestingOutputPeek { } } + /** + * @override + */ + public currentTest() { + return this.test; + } + /** * @override */ diff --git a/src/vs/workbench/contrib/testing/common/configuration.ts b/src/vs/workbench/contrib/testing/common/configuration.ts index 0630b3f40ac..546ec5ff9da 100644 --- a/src/vs/workbench/contrib/testing/common/configuration.ts +++ b/src/vs/workbench/contrib/testing/common/configuration.ts @@ -8,7 +8,14 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry'; export const enum TestingConfigKeys { - AutoRunDelay = 'testing.autoRun.delay' + AutoRunDelay = 'testing.autoRun.delay', + AutoOpenPeekView = 'testing.automaticallyOpenPeekView', + AutoOpenPeekViewDuringAutoRun = 'testing.automaticallyOpenPeekViewDuringAutoRun', +} + +export const enum AutoOpenPeekViewWhen { + FailureVisible = 'failureInVisibleDocument', + FailureAnywhere = 'failureAnywhere', } export const testingConfiguation: IConfigurationNode = { @@ -23,11 +30,30 @@ export const testingConfiguation: IConfigurationNode = { description: localize('testing.autoRun.delay', "How long to wait, in milliseconds, after a test is marked as outdated and starting a new run."), default: 1000, }, + [TestingConfigKeys.AutoOpenPeekView]: { + description: localize('testing.automaticallyOpenPeekView', "Configures when the error peek view is automatically opened."), + enum: [ + AutoOpenPeekViewWhen.FailureAnywhere, + AutoOpenPeekViewWhen.FailureVisible, + ], + default: AutoOpenPeekViewWhen.FailureVisible, + enumDescriptions: [ + localize('testing.automaticallyOpenPeekView.failureAnywhere', "Open automatically no matter where the failure is."), + localize('testing.automaticallyOpenPeekView.failureInVisibleDocument', "Open automatically when a test fails in a visible document.") + ], + }, + [TestingConfigKeys.AutoOpenPeekViewDuringAutoRun]: { + description: localize('testing.automaticallyOpenPeekViewDuringAutoRun', "Controls whether to automatically open the peek view during auto-run mode."), + type: 'boolean', + default: false, + } } }; export interface ITestingConfiguration { [TestingConfigKeys.AutoRunDelay]: number; + [TestingConfigKeys.AutoOpenPeekView]: AutoOpenPeekViewWhen; + [TestingConfigKeys.AutoOpenPeekViewDuringAutoRun]: boolean; } export const getTestingConfiguration = (config: IConfigurationService, key: K) => config.getValue(key); diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index 817cef54b7e..0e6ebdae3ba 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/src/vs/workbench/contrib/testing/common/constants.ts @@ -25,6 +25,12 @@ export const enum TestExplorerViewSorting { ByName = 'name', } +export const enum TestExplorerStateFilter { + OnlyFailed = 'failed', + OnlyExecuted = 'excuted', + All = 'all', +} + export const testStateNames: { [K in TestRunState]: string } = { [TestRunState.Errored]: localize('testState.errored', 'Errored'), [TestRunState.Failed]: localize('testState.failed', 'Failed'), diff --git a/src/vs/workbench/contrib/testing/common/testCollection.ts b/src/vs/workbench/contrib/testing/common/testCollection.ts index 8ceb490a2dc..e42f8a207a9 100644 --- a/src/vs/workbench/contrib/testing/common/testCollection.ts +++ b/src/vs/workbench/contrib/testing/common/testCollection.ts @@ -5,7 +5,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { URI } from 'vs/base/common/uri'; -import { Location as ModeLocation } from 'vs/editor/common/modes'; +import { Range } from 'vs/editor/common/core/range'; import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol'; import { TestMessageSeverity, TestRunState } from 'vs/workbench/api/common/extHostTypes'; @@ -15,11 +15,12 @@ export interface TestIdWithProvider { } /** - * Request to them main thread to run a set of tests. + * Request to the main thread to run a set of tests. */ export interface RunTestsRequest { tests: TestIdWithProvider[]; debug: boolean; + isAutoRun?: boolean; } /** @@ -32,12 +33,20 @@ export interface RunTestForProviderRequest { debug: boolean; } +/** + * Location with a fully-instantiated Range and URI. + */ +export interface IRichLocation { + range: Range; + uri: URI; +} + export interface ITestMessage { message: string | IMarkdownString; severity: TestMessageSeverity | undefined; expectedOutput: string | undefined; actualOutput: string | undefined; - location: ModeLocation | undefined; + location: IRichLocation | undefined; } export interface ITestState { @@ -54,7 +63,7 @@ export interface ITestItem { extId: string; label: string; children?: never; - location: ModeLocation | undefined; + location: IRichLocation | undefined; description: string | undefined; runnable: boolean; debuggable: boolean; diff --git a/src/vs/workbench/contrib/testing/common/testResultService.ts b/src/vs/workbench/contrib/testing/common/testResultService.ts index bc2cbda0ca8..c380e53708b 100644 --- a/src/vs/workbench/contrib/testing/common/testResultService.ts +++ b/src/vs/workbench/contrib/testing/common/testResultService.ts @@ -6,13 +6,14 @@ import { Emitter, Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; +import { Range } from 'vs/editor/common/core/range'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { TestRunState } from 'vs/workbench/api/common/extHostTypes'; import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; -import { IncrementalTestCollectionItem, ITestState, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection'; +import { IncrementalTestCollectionItem, ITestState, RunTestsRequest } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { statesInOrder } from 'vs/workbench/contrib/testing/common/testingStates'; import { IMainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testService'; @@ -22,6 +23,18 @@ import { IMainThreadTestCollection } from 'vs/workbench/contrib/testing/common/t */ export type TestStateCount = { [K in TestRunState]: number }; +export const enum TestResultItemChangeReason { + Retired, + ParentRetired, + ComputedStateChange, + OwnStateChange, +} + +export type TestResultItemChange = { item: TestResultItem; result: ITestResult } & ( + | { reason: TestResultItemChangeReason.Retired | TestResultItemChangeReason.ParentRetired | TestResultItemChangeReason.ComputedStateChange } + | { reason: TestResultItemChangeReason.OwnStateChange; previous: ITestState } +); + export interface ITestResult { /** * Count of the number of tests in each run state. @@ -38,6 +51,11 @@ export interface ITestResult { */ readonly isComplete: boolean; + /** + * Whether this test result is triggered from an auto run. + */ + readonly isAutoRun?: boolean; + /** * Gets the state of the test by its extension-assigned ID. */ @@ -50,7 +68,7 @@ export interface ITestResult { toJSON(): ISerializedResults; } -const makeEmptyCounts = () => { +export const makeEmptyCounts = () => { const o: Partial = {}; for (const state of statesInOrder) { o[state] = 0; @@ -148,11 +166,10 @@ const makeNodeAndChildren = ( interface ISerializedResults { id: string; - counts: TestStateCount; - items: Iterable<[extId: string, item: TestResultItem]>; + items: (Omit & { children: string[], retired: undefined })[]; } -interface TestResultItem extends IncrementalTestCollectionItem { +export interface TestResultItem extends IncrementalTestCollectionItem { state: ITestState; computedState: TestRunState; retired: boolean; @@ -169,11 +186,11 @@ export class LiveTestResult implements ITestResult { */ public static from( collections: ReadonlyArray, - tests: ReadonlyArray, + req: RunTestsRequest, ) { const testByExtId = new Map(); const testByInternalId = new Map(); - for (const test of tests) { + for (const test of req.tests) { for (const collection of collections) { const node = collection.getNodeById(test.testId); if (!node) { @@ -185,15 +202,13 @@ export class LiveTestResult implements ITestResult { } } - return new LiveTestResult(collections, testByExtId, testByInternalId); + return new LiveTestResult(collections, testByExtId, testByInternalId, !!req.isAutoRun); } private readonly completeEmitter = new Emitter(); - private readonly retireEmitter = new Emitter(); - private readonly changeEmitter = new Emitter(); + private readonly changeEmitter = new Emitter(); private _complete = false; - public readonly onRetired = this.retireEmitter.event; public readonly onChange = this.changeEmitter.event; public readonly onComplete = this.completeEmitter.event; @@ -256,6 +271,7 @@ export class LiveTestResult implements ITestResult { private readonly collections: ReadonlyArray, private readonly testByExtId: Map, private readonly testByInternalId: Map, + public readonly isAutoRun: boolean, ) { this.counts[TestRunState.Unset] = testByInternalId.size; } @@ -273,10 +289,7 @@ export class LiveTestResult implements ITestResult { public setAllToState(state: ITestState, when: (_t: TestResultItem) => boolean) { for (const test of this.testByInternalId.values()) { if (when(test)) { - this.counts[state.state]--; - test.state = state; - this.counts[state.state]++; - refreshComputedState(this.computedStateAccessor, test, t => this.changeEmitter.fire(t)); + this.fireUpdateAndRefresh(test, state); } } } @@ -293,15 +306,22 @@ export class LiveTestResult implements ITestResult { return; } - if (state.state === entry.state.state) { - entry.state = state; - this.changeEmitter.fire(entry); - } else { - this.counts[entry.state.state]--; - entry.state = state; - this.counts[entry.state.state]++; - refreshComputedState(this.computedStateAccessor, entry, t => this.changeEmitter.fire(t)); + this.fireUpdateAndRefresh(entry, state); + } + + private fireUpdateAndRefresh(entry: TestResultItem, newState: ITestState) { + const previous = entry.state; + entry.state = newState; + + if (newState.state !== previous.state) { + this.counts[previous.state]--; + this.counts[newState.state]++; + refreshComputedState(this.computedStateAccessor, entry, t => ( + t !== entry && this.changeEmitter.fire({ item: t, result: this, reason: TestResultItemChangeReason.ComputedStateChange }) + )); } + + this.changeEmitter.fire({ item: entry, result: this, reason: TestResultItemChangeReason.OwnStateChange, previous }); } /** @@ -313,7 +333,6 @@ export class LiveTestResult implements ITestResult { return; } - this.retireEmitter.fire(root); const queue: Iterable[] = [[root.id]]; while (queue.length) { for (const id of queue.pop()!) { @@ -321,7 +340,13 @@ export class LiveTestResult implements ITestResult { if (entry && !entry.retired) { entry.retired = true; queue.push(entry.children); - this.changeEmitter.fire(entry); + this.changeEmitter.fire({ + result: this, + item: entry, + reason: entry === root + ? TestResultItemChangeReason.Retired + : TestResultItemChangeReason.ParentRetired + }); } } } @@ -336,7 +361,11 @@ export class LiveTestResult implements ITestResult { for (const collection of this.collections) { let test = collection.getNodeById(testId); if (test) { - return makeNodeAndChildren(collection, test, this.testByExtId, this.testByInternalId); + const originalSize = this.testByExtId.size; + makeParents(collection, test, this.testByExtId, this.testByInternalId); + const node = makeNodeAndChildren(collection, test, this.testByExtId, this.testByInternalId); + this.counts[TestRunState.Unset] += this.testByExtId.size - originalSize; + return node; } } @@ -361,7 +390,14 @@ export class LiveTestResult implements ITestResult { * @inheritdoc */ public toJSON(): ISerializedResults { - return { id: this.id, counts: this.counts, items: [...this.testByExtId.entries()] }; + return { + id: this.id, + items: [...this.testByExtId.values()].map(entry => ({ + ...entry, + retired: undefined, + children: [...entry.children], + })), + }; } } @@ -372,30 +408,39 @@ class HydratedTestResult implements ITestResult { /** * @inheritdoc */ - public readonly counts = this.serialized.counts; + public readonly counts = makeEmptyCounts(); /** * @inheritdoc */ - public readonly id = this.serialized.id; + public readonly id: string; /** * @inheritdoc */ public readonly isComplete = true; - private readonly map = new Map(); + private readonly byExtId = new Map(); constructor(private readonly serialized: ISerializedResults) { - for (const [key, value] of serialized.items) { - this.map.set(key, value); + this.id = serialized.id; - value.retired = true; - for (const message of value.state.messages) { + for (const item of serialized.items) { + const cast: TestResultItem = { ...item, retired: true, children: new Set(item.children) }; + if (cast.item.location) { + cast.item.location.uri = URI.revive(cast.item.location.uri); + cast.item.location.range = Range.lift(cast.item.location.range); + } + + for (const message of cast.state.messages) { if (message.location) { message.location.uri = URI.revive(message.location.uri); + message.location.range = Range.lift(message.location.range); } } + + this.counts[item.state.state]++; + this.byExtId.set(item.item.extId, cast); } } @@ -403,7 +448,7 @@ class HydratedTestResult implements ITestResult { * @inheritdoc */ public getStateByExtId(extTestId: string) { - return this.map.get(extTestId); + return this.byExtId.get(extTestId); } /** @@ -429,12 +474,7 @@ export interface ITestResultService { /** * Fired when a test changed it state, or its computed state is updated. */ - readonly onTestChanged: Event<[results: ITestResult, item: TestResultItem]>; - - /** - * Fired when a test is retired, in addition to `onTestChanged`. - */ - readonly onTestRetired: Event; + readonly onTestChanged: Event; /** * List of known test results. @@ -469,8 +509,7 @@ const RETAIN_LAST_RESULTS = 64; export class TestResultService implements ITestResultService { declare _serviceBrand: undefined; private changeResultEmitter = new Emitter(); - private testRetiredEmitter = new Emitter(); - private testChangeEmitter = new Emitter<[results: ITestResult, item: TestResultItem]>(); + private testChangeEmitter = new Emitter(); /** * @inheritdoc @@ -487,11 +526,6 @@ export class TestResultService implements ITestResultService { */ public readonly onTestChanged = this.testChangeEmitter.event; - /** - * @inheritdoc - */ - public readonly onTestRetired = this.testRetiredEmitter.event; - private readonly isRunning: IContextKey; private readonly serializedResults: StoredValue; @@ -503,8 +537,12 @@ export class TestResultService implements ITestResultService { target: StorageTarget.MACHINE }, storage); - for (const value of this.serializedResults.get([])) { - this.results.push(new HydratedTestResult(value)); + try { + for (const value of this.serializedResults.get([])) { + this.results.push(new HydratedTestResult(value)); + } + } catch (e) { + // outdated structure } } @@ -532,8 +570,7 @@ export class TestResultService implements ITestResultService { } result.onComplete(() => this.onComplete(result)); - result.onChange(t => this.testChangeEmitter.fire([result, t]), this.testChangeEmitter); - result.onRetired(this.testRetiredEmitter.fire, this.testRetiredEmitter); + result.onChange(this.testChangeEmitter.fire, this.testChangeEmitter); this.isRunning.set(true); this.changeResultEmitter.fire({ started: result }); result.setAllToState(queuedState, () => true); @@ -568,7 +605,7 @@ export class TestResultService implements ITestResultService { private onComplete(result: LiveTestResult) { // move the complete test run down behind any still-running ones - for (let i = 0; i < this.results.length - 2; i++) { + for (let i = 0; i < this.results.length - 1; i++) { if (this.results[i].isComplete && !this.results[i + 1].isComplete) { [this.results[i], this.results[i + 1]] = [this.results[i + 1], this.results[i]]; } diff --git a/src/vs/workbench/contrib/testing/common/testService.ts b/src/vs/workbench/contrib/testing/common/testService.ts index 61639ba1874..8d1e5221dde 100644 --- a/src/vs/workbench/contrib/testing/common/testService.ts +++ b/src/vs/workbench/contrib/testing/common/testService.ts @@ -74,6 +74,23 @@ export const waitForAllRoots = (collection: IMainThreadTestCollection, timeout = }).finally(() => listener.dispose()); }; +export const waitForAllTests = (collection: IMainThreadTestCollection, timeout = 3000) => { + if (collection.busyProviders === 0) { + return Promise.resolve(); + } + + let listener: IDisposable; + return new Promise(resolve => { + listener = collection.onBusyProvidersChange(count => { + if (count === 0) { + resolve(); + } + }); + + setTimeout(resolve, timeout); + }).finally(() => listener.dispose()); +}; + export interface ITestService { readonly _serviceBrand: undefined; readonly onShouldSubscribe: Event<{ resource: ExtHostTestingResource, uri: URI; }>; diff --git a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index 8a18a20daa5..95d4a9e7bc8 100644 --- a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts @@ -124,7 +124,7 @@ export class TestService extends Disposable implements ITestService { const subscriptions = [...this.testSubscriptions.values()] .filter(v => req.tests.some(t => v.collection.getNodeById(t.testId))) .map(s => this.subscribeToDiffs(s.ident.resource, s.ident.uri)); - const result = this.testResults.push(LiveTestResult.from(subscriptions.map(s => s.object), req.tests)); + const result = this.testResults.push(LiveTestResult.from(subscriptions.map(s => s.object), req)); try { const tests = groupBy(req.tests, (a, b) => a.providerId === b.providerId ? 0 : 1); @@ -242,7 +242,7 @@ export class TestService extends Disposable implements ITestService { } } -class MainThreadTestCollection extends AbstractIncrementalTestCollection implements IMainThreadTestCollection { +export class MainThreadTestCollection extends AbstractIncrementalTestCollection implements IMainThreadTestCollection { private pendingRootChangeEmitter = new Emitter(); private busyProvidersChangeEmitter = new Emitter(); private _busyProviders = 0; diff --git a/src/vs/workbench/contrib/testing/common/testingAutoRun.ts b/src/vs/workbench/contrib/testing/common/testingAutoRun.ts index 360bbf09c31..a1eae07dd53 100644 --- a/src/vs/workbench/contrib/testing/common/testingAutoRun.ts +++ b/src/vs/workbench/contrib/testing/common/testingAutoRun.ts @@ -13,7 +13,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; -import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; +import { ITestResultService, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { IWorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; @@ -82,7 +82,7 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun { isRunning = true; rerunIds.clear(); - await this.testService.runTests({ debug: false, tests }); + await this.testService.runTests({ debug: false, tests, isAutoRun: true }); isRunning = false; if (rerunIds.size > 0) { @@ -90,10 +90,15 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun { } }, delay)); - store.add(this.results.onTestRetired(test => { + store.add(this.results.onTestChanged(evt => { + if (evt.reason !== TestResultItemChangeReason.Retired) { + return; + } + + const { extId } = evt.item.item; const workspaceTest = mapFind(workspaceTests.workspaceFolderCollections, - ([, c]) => c.getNodeById(test.id) ?? Iterable.find(c.all, t => t.item.extId === test.item.extId)); - const subject = workspaceTest ?? test; + ([, c]) => c.getNodeById(evt.item.id) ?? Iterable.find(c.all, t => t.item.extId === extId)); + const subject = workspaceTest ?? evt.item; rerunIds.set(subject.id, ({ testId: subject.id, providerId: subject.providerId })); diff --git a/src/vs/workbench/contrib/testing/common/testingContentProvider.ts b/src/vs/workbench/contrib/testing/common/testingContentProvider.ts index c63a507d15f..b3e91103ce4 100644 --- a/src/vs/workbench/contrib/testing/common/testingContentProvider.ts +++ b/src/vs/workbench/contrib/testing/common/testingContentProvider.ts @@ -38,7 +38,7 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode return null; } - const test = this.resultService.getResult(parsed.resultId)?.getStateByExtId(parsed.testId); + const test = this.resultService.getResult(parsed.resultId)?.getStateByExtId(parsed.testExtId); if (!test) { return null; diff --git a/src/vs/workbench/contrib/testing/common/testingUri.ts b/src/vs/workbench/contrib/testing/common/testingUri.ts index 5f99df72ba0..9503c99e152 100644 --- a/src/vs/workbench/contrib/testing/common/testingUri.ts +++ b/src/vs/workbench/contrib/testing/common/testingUri.ts @@ -15,7 +15,7 @@ export const enum TestUriType { interface IResultTestUri { resultId: string; - testId: string; + testExtId: string; } interface IResultTestMessageReference extends IResultTestUri { @@ -43,19 +43,20 @@ const enum TestUriParts { export const parseTestUri = (uri: URI): ParsedTestUri | undefined => { const type = uri.authority; - const [locationId, testId, ...request] = uri.path.slice(1).split('/'); + const [locationId, ...request] = uri.path.slice(1).split('/'); if (request[0] === TestUriParts.Messages) { const index = Number(request[1]); const part = request[2]; + const testExtId = uri.query; if (type === TestUriParts.Results) { switch (part) { case TestUriParts.Text: - return { resultId: locationId, testId, messageIndex: index, type: TestUriType.ResultMessage }; + return { resultId: locationId, testExtId, messageIndex: index, type: TestUriType.ResultMessage }; case TestUriParts.ActualOutput: - return { resultId: locationId, testId, messageIndex: index, type: TestUriType.ResultActualOutput }; + return { resultId: locationId, testExtId, messageIndex: index, type: TestUriType.ResultActualOutput }; case TestUriParts.ExpectedOutput: - return { resultId: locationId, testId, messageIndex: index, type: TestUriType.ResultExpectedOutput }; + return { resultId: locationId, testExtId, messageIndex: index, type: TestUriType.ResultExpectedOutput }; } } } @@ -71,7 +72,8 @@ export const buildTestUri = (parsed: ParsedTestUri): URI => { const msgRef = (locationId: string, index: number, ...remaining: string[]) => URI.from({ ...uriParts, - path: ['', locationId, parsed.testId, TestUriParts.Messages, index, ...remaining].join('/'), + query: parsed.testExtId, + path: ['', locationId, TestUriParts.Messages, index, ...remaining].join('/'), }); switch (parsed.type) { diff --git a/src/vs/workbench/contrib/testing/test/common/ownedTestCollection.ts b/src/vs/workbench/contrib/testing/test/common/ownedTestCollection.ts index a0f841e4b3e..8df8bb684ff 100644 --- a/src/vs/workbench/contrib/testing/test/common/ownedTestCollection.ts +++ b/src/vs/workbench/contrib/testing/test/common/ownedTestCollection.ts @@ -5,6 +5,8 @@ import { OwnedTestCollection, SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; import { TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testServiceImpl'; +import { testStubs } from 'vs/workbench/contrib/testing/common/testStubs'; export class TestSingleUseCollection extends SingleUseTestCollection { private idCounter = 0; @@ -35,3 +37,15 @@ export class TestOwnedTestCollection extends OwnedTestCollection { return new TestSingleUseCollection(this.testIdToInternal, publishDiff); } } + +/** + * Gets a main thread test collection initialized with the given set of + * roots/stubs. + */ +export const getInitializedMainTestCollection = (root = testStubs.nested()) => { + const c = new MainThreadTestCollection(0); + const singleUse = new TestSingleUseCollection(new Map(), () => undefined); + singleUse.addRoot(root, 'provider'); + c.apply(singleUse.collectDiff()); + return c; +}; diff --git a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts new file mode 100644 index 00000000000..86851a62f51 --- /dev/null +++ b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts @@ -0,0 +1,194 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { LiveTestResult, makeEmptyCounts, TestResultItemChange, TestResultItemChangeReason, TestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; +import { ReExportedTestRunState as TestRunState } from 'vs/workbench/contrib/testing/common/testStubs'; +import { getInitializedMainTestCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; +import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; + +suite('Workbench - Test Results Service', () => { + const getLabelsIn = (it: Iterable) => [...it].map(t => t.item.label).sort(); + const getChangeSummary = () => [...changed] + .map(c => ({ reason: c.reason, label: c.item.item.label })) + .sort((a, b) => a.label.localeCompare(b.label)); + + let r: LiveTestResult; + let changed = new Set(); + + setup(() => { + changed = new Set(); + r = LiveTestResult.from( + [getInitializedMainTestCollection()], + { tests: [{ providerId: 'provider', testId: '1' }], debug: false } + ); + + r.onChange(e => changed.add(e)); + }); + + suite('LiveTestResult', () => { + test('is empty if no tests are requesteed', () => { + const r = LiveTestResult.from([getInitializedMainTestCollection()], { tests: [], debug: false }); + assert.deepStrictEqual(getLabelsIn(r.tests), []); + }); + + test('does not change or retire initially', () => { + assert.deepStrictEqual(0, changed.size); + }); + + test('initializes with the subtree of requested tests', () => { + assert.deepStrictEqual(getLabelsIn(r.tests), ['a', 'aa', 'ab', 'root']); + }); + + test('initializes with valid counts', () => { + assert.deepStrictEqual(r.counts, { + ...makeEmptyCounts(), + [TestRunState.Unset]: 4 + }); + }); + + test('setAllToState', () => { + r.setAllToState({ state: TestRunState.Queued, duration: 0, messages: [] }, t => t.item.label !== 'root'); + assert.deepStrictEqual(r.counts, { + ...makeEmptyCounts(), + [TestRunState.Unset]: 1, + [TestRunState.Queued]: 3, + }); + + assert.deepStrictEqual(r.getStateByExtId('root\0a')?.state.state, TestRunState.Queued); + assert.deepStrictEqual(getChangeSummary(), [ + { label: 'a', reason: TestResultItemChangeReason.OwnStateChange }, + { label: 'aa', reason: TestResultItemChangeReason.OwnStateChange }, + { label: 'ab', reason: TestResultItemChangeReason.OwnStateChange }, + { label: 'root', reason: TestResultItemChangeReason.ComputedStateChange }, + ]); + }); + + test('updateState', () => { + r.updateState('1', { state: TestRunState.Running, duration: 0, messages: [] }); + assert.deepStrictEqual(r.counts, { + ...makeEmptyCounts(), + [TestRunState.Running]: 1, + [TestRunState.Unset]: 3, + }); + assert.deepStrictEqual(r.getStateByExtId('root\0a')?.state.state, TestRunState.Running); + // update computed state: + assert.deepStrictEqual(r.getStateByExtId('root')?.computedState, TestRunState.Running); + assert.deepStrictEqual(getChangeSummary(), [ + { label: 'a', reason: TestResultItemChangeReason.OwnStateChange }, + { label: 'root', reason: TestResultItemChangeReason.ComputedStateChange }, + ]); + }); + + test('retire', () => { + r.retire('root\0a'); + assert.deepStrictEqual(getChangeSummary(), [ + { label: 'a', reason: TestResultItemChangeReason.Retired }, + { label: 'aa', reason: TestResultItemChangeReason.ParentRetired }, + { label: 'ab', reason: TestResultItemChangeReason.ParentRetired }, + ]); + + changed.clear(); + r.retire('root\0a'); + assert.strictEqual(changed.size, 0); + }); + + test('addTestToRun', () => { + r.updateState('4', { state: TestRunState.Running, duration: 0, messages: [] }); + assert.deepStrictEqual(r.counts, { + ...makeEmptyCounts(), + [TestRunState.Running]: 1, + [TestRunState.Unset]: 4, + }); + assert.deepStrictEqual(r.getStateByExtId('root\0b')?.state.state, TestRunState.Running); + // update computed state: + assert.deepStrictEqual(r.getStateByExtId('root')?.computedState, TestRunState.Running); + }); + + test('markComplete', () => { + r.setAllToState({ state: TestRunState.Queued, duration: 0, messages: [] }, t => true); + r.updateState('2', { state: TestRunState.Passed, duration: 0, messages: [] }); + changed.clear(); + + r.markComplete(); + + assert.deepStrictEqual(r.counts, { + ...makeEmptyCounts(), + [TestRunState.Passed]: 1, + [TestRunState.Unset]: 3, + }); + + assert.deepStrictEqual(r.getStateByExtId('root')?.state.state, TestRunState.Unset); + assert.deepStrictEqual(r.getStateByExtId('root\0a\0aa')?.state.state, TestRunState.Passed); + }); + }); + + suite('service', () => { + let storage: TestStorageService; + let results: TestResultService; + + setup(() => { + storage = new TestStorageService(); + results = new TestResultService( + new MockContextKeyService(), + storage, + ); + }); + + test('pushes new result', () => { + results.push(r); + assert.deepStrictEqual(results.results, [r]); + }); + + test('serializes and re-hydrates', () => { + results.push(r); + r.updateState('2', { state: TestRunState.Passed, duration: 0, messages: [] }); + r.markComplete(); + + results = new TestResultService( + new MockContextKeyService(), + storage, + ); + + const [rehydrated, actual] = results.getStateByExtId('root')!; + const expected = r.getStateByExtId('root')!; + delete expected.state.duration; // delete undefined props that don't survive serialization + delete expected.item.location; + + assert.deepStrictEqual(actual, { ...expected, retired: true }); + assert.deepStrictEqual(rehydrated.counts, r.counts); + assert.strictEqual(rehydrated.isComplete, true); + }); + + test('clears results but keeps ongoing tests', () => { + results.push(r); + r.markComplete(); + + const r2 = results.push(LiveTestResult.from( + [getInitializedMainTestCollection()], + { tests: [{ providerId: 'provider', testId: '1' }], debug: false } + )); + results.clear(); + + assert.deepStrictEqual(results.results, [r2]); + }); + + test('keeps ongoing tests on top', () => { + results.push(r); + const r2 = results.push(LiveTestResult.from( + [getInitializedMainTestCollection()], + { tests: [{ providerId: 'provider', testId: '1' }], debug: false } + )); + + assert.deepStrictEqual(results.results, [r2, r]); + r2.markComplete(); + assert.deepStrictEqual(results.results, [r, r2]); + r.markComplete(); + assert.deepStrictEqual(results.results, [r, r2]); + }); + }); +}); diff --git a/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts b/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts index 41d534e5d69..e9c1d60412e 100644 --- a/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts @@ -9,9 +9,9 @@ import { buildTestUri, ParsedTestUri, parseTestUri, TestUriType } from 'vs/workb suite('Workbench - Testing URIs', () => { test('round trip', () => { const uris: ParsedTestUri[] = [ - { type: TestUriType.ResultActualOutput, messageIndex: 42, resultId: 'r', testId: 't' }, - { type: TestUriType.ResultExpectedOutput, messageIndex: 42, resultId: 'r', testId: 't' }, - { type: TestUriType.ResultMessage, messageIndex: 42, resultId: 'r', testId: 't' }, + { type: TestUriType.ResultActualOutput, messageIndex: 42, resultId: 'r', testExtId: 't' }, + { type: TestUriType.ResultExpectedOutput, messageIndex: 42, resultId: 'r', testExtId: 't' }, + { type: TestUriType.ResultMessage, messageIndex: 42, resultId: 'r', testExtId: 't' }, ]; for (const uri of uris) { diff --git a/src/vs/workbench/contrib/url/browser/trustedDomains.ts b/src/vs/workbench/contrib/url/browser/trustedDomains.ts index b5188bfb036..145afdf032c 100644 --- a/src/vs/workbench/contrib/url/browser/trustedDomains.ts +++ b/src/vs/workbench/contrib/url/browser/trustedDomains.ts @@ -206,7 +206,7 @@ export async function readWorkspaceTrustedDomains(accessor: ServicesAccessor): P export async function readAuthenticationTrustedDomains(accessor: ServicesAccessor): Promise { const authenticationService = accessor.get(IAuthenticationService); - return authenticationService.isAuthenticationProviderRegistered('github') && ((await authenticationService.getAllSessions('github')) ?? []).length > 0 + return authenticationService.isAuthenticationProviderRegistered('github') && ((await authenticationService.getSessions('github')) ?? []).length > 0 ? [`https://github.com`] : []; } diff --git a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts index d67cf68e348..102f7132e9c 100644 --- a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts +++ b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts @@ -60,10 +60,13 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv return !!this._webview.value?.isFocused; } + private _isDisposed = false; + private readonly _onDidDispose = this._register(new Emitter()); public onDidDispose = this._onDidDispose.event; dispose() { + this._isDisposed = true; this.container.remove(); this._onDidDispose.fire(); super.dispose(); @@ -138,6 +141,10 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv } private show() { + if (this._isDisposed) { + throw new Error('Webview overlay is disposed'); + } + if (!this._webview.value) { const webview = this._webviewService.createWebviewElement(this.id, this._options, this._contentOptions, this.extension); this._webview.value = webview; diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index b2f093ee195..2d4ac83084a 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -446,6 +446,8 @@ host.onMessage('focus', () => { const activeFrame = getActiveFrame(); if (!activeFrame || !activeFrame.contentWindow) { + // Focus the top level webview instead + window.focus(); return; } @@ -589,6 +591,10 @@ contentWindow.addEventListener('scroll', handleInnerScroll); contentWindow.addEventListener('wheel', handleWheel); + if (document.hasFocus()) { + contentWindow.focus(); + } + pendingMessages.forEach((data) => { contentWindow.postMessage(data, '*'); }); diff --git a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js index 1030c047421..f6c93682fea 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js +++ b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js @@ -2,13 +2,18 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// @ts-check + +/// /// +const sw = /** @type {ServiceWorkerGlobalScope} */ (/** @type {any} */ (self)); + const VERSION = 1; const resourceCacheName = `vscode-resource-cache-${VERSION}`; -const rootPath = self.location.pathname.replace(/\/service-worker.js$/, ''); +const rootPath = sw.location.pathname.replace(/\/service-worker.js$/, ''); /** * Root path for resources @@ -101,11 +106,12 @@ const localhostRequestStore = new RequestStore(); const notFound = () => new Response('Not Found', { status: 404, }); -self.addEventListener('message', async (event) => { +sw.addEventListener('message', async (event) => { switch (event.data.channel) { case 'version': { - self.clients.get(event.source.id).then(client => { + const source = /** @type {Client} */ (event.source); + sw.clients.get(source.id).then(client => { if (client) { client.postMessage({ channel: 'version', @@ -153,26 +159,26 @@ self.addEventListener('message', async (event) => { console.log('Unknown message'); }); -self.addEventListener('fetch', (event) => { +sw.addEventListener('fetch', (event) => { const requestUrl = new URL(event.request.url); // See if it's a resource request - if (requestUrl.origin === self.origin && requestUrl.pathname.startsWith(resourceRoot + '/')) { + if (requestUrl.origin === sw.origin && requestUrl.pathname.startsWith(resourceRoot + '/')) { return event.respondWith(processResourceRequest(event, requestUrl)); } // See if it's a localhost request - if (requestUrl.origin !== self.origin && requestUrl.host.match(/^localhost:(\d+)$/)) { + if (requestUrl.origin !== sw.origin && requestUrl.host.match(/^localhost:(\d+)$/)) { return event.respondWith(processLocalhostRequest(event, requestUrl)); } }); -self.addEventListener('install', (event) => { - event.waitUntil(self.skipWaiting()); // Activate worker immediately +sw.addEventListener('install', (event) => { + event.waitUntil(sw.skipWaiting()); // Activate worker immediately }); -self.addEventListener('activate', (event) => { - event.waitUntil(self.clients.claim()); // Become available to all pages +sw.addEventListener('activate', (event) => { + event.waitUntil(sw.clients.claim()); // Become available to all pages }); /** @@ -180,7 +186,7 @@ self.addEventListener('activate', (event) => { * @param {URL} requestUrl */ async function processResourceRequest(event, requestUrl) { - const client = await self.clients.get(event.clientId); + const client = await sw.clients.get(event.clientId); if (!client) { console.log('Could not find inner client for request'); return notFound(); @@ -252,7 +258,7 @@ async function processResourceRequest(event, requestUrl) { * @param {URL} requestUrl */ async function processLocalhostRequest(event, requestUrl) { - const client = await self.clients.get(event.clientId); + const client = await sw.clients.get(event.clientId); if (!client) { // This is expected when requesting resources on other localhost ports // that are not spawned by vs code @@ -299,7 +305,7 @@ function getWebviewIdForClient(client) { } async function getOuterIframeClient(webviewId) { - const allClients = await self.clients.matchAll({ includeUncontrolled: true }); + const allClients = await sw.clients.matchAll({ includeUncontrolled: true }); return allClients.find(client => { const clientUrl = new URL(client.url); return (clientUrl.pathname === `${rootPath}/` || clientUrl.pathname === `${rootPath}/index.html`) && clientUrl.search.match(new RegExp('\\bid=' + webviewId)); diff --git a/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts b/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts index 672018e2ce4..5321866d575 100644 --- a/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts @@ -128,8 +128,16 @@ export class ElectronIframeWebview extends IFrameWebview { return; } + // Clear the existing focus first if not already on the webview. + // This is required because the next part where we set the focus is async. + if (document.activeElement && document.activeElement instanceof HTMLElement && document.activeElement !== this.element) { + // Don't blur if on the webview because this will also happen async and may unset the focus + // after the focus trigger fires below. + document.activeElement.blur(); + } + // Workaround for https://github.com/microsoft/vscode/issues/75209 - // .focus is async for imframes so for a sequence of actions such as: + // Electron's webview.focus is async so for a sequence of actions such as: // // 1. Open webview // 1. Show quick pick from command palette @@ -143,8 +151,7 @@ export class ElectronIframeWebview extends IFrameWebview { if (!this.isFocused || !this.element) { return; } - - if (document.activeElement?.tagName === 'INPUT') { + if (document.activeElement && document.activeElement?.tagName !== 'BODY') { return; } try { diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts index 1eebb76e499..5a6f5253013 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts @@ -31,6 +31,7 @@ export class WebviewEditor extends EditorPane { private _element?: HTMLElement; private _dimension?: DOM.Dimension; private _visible = false; + private _isDisposed = false; private readonly _webviewVisibleDisposables = this._register(new DisposableStore()); private readonly _onFocusWindowHandler = this._register(new MutableDisposable()); @@ -71,6 +72,8 @@ export class WebviewEditor extends EditorPane { } public dispose(): void { + this._isDisposed = true; + if (this._element) { this._element.remove(); this._element = undefined; @@ -133,7 +136,7 @@ export class WebviewEditor extends EditorPane { await super.setInput(input, options, context, token); await input.resolve(); - if (token.isCancellationRequested) { + if (token.isCancellationRequested || this._isDisposed) { return; } diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts index cb7bcf8f3c9..f890c8f273e 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts @@ -242,6 +242,11 @@ export class GettingStartedPage extends EditorPane { mediaElement.setAttribute('src', ''); mediaElement.setAttribute('alt', ''); } + setTimeout(() => { + // rescan after animation finishes + this.detailsScrollbar?.scanDomNode(); + this.detailImageScrollbar?.scanDomNode(); + }, 100); this.detailsScrollbar?.scanDomNode(); this.detailImageScrollbar?.scanDomNode(); } @@ -302,7 +307,9 @@ export class GettingStartedPage extends EditorPane { $('.category-description.description', { 'aria-label': category.description + ' ' + localize('pressEnterToSelect', "Press Enter to Select") }, category.description), $('.category-progress', { 'x-data-category-id': category.id, }, $('.message'), - $('.progress-bar-outer', {}, + $('.progress-bar-outer', { + 'role': 'progressbar' + }, $('.progress-bar-inner')))) : $('.category-description-container', {}, @@ -310,12 +317,15 @@ export class GettingStartedPage extends EditorPane { $('.category-description.description', { 'aria-label': category.description + ' ' + localize('pressEnterToSelect', "Press Enter to Select") }, category.description)); return $('button.getting-started-category', - { 'x-dispatch': 'selectCategory:' + category.id, }, + { + 'x-dispatch': 'selectCategory:' + category.id, + 'role': 'listitem', + }, $(ThemeIcon.asCSSSelector(category.icon), {}), categoryDescriptionElement); }); const categoryScrollContainer = $('.getting-started-categories-scrolling-container'); - const categoriesContainer = $('.getting-started-categories-container'); + const categoriesContainer = $('.getting-started-categories-container', { 'role': 'list' }); categoryElements.forEach(element => { categoriesContainer.appendChild(element); }); @@ -324,7 +334,7 @@ export class GettingStartedPage extends EditorPane { const showOnStartupCheckbox = $('input.checkbox', { id: 'showOnStartup', type: 'checkbox' }) as HTMLInputElement; categoryScrollContainer.appendChild( $('.footer', {}, - $('button.skip.button-link', { 'x-dispatch': 'skip' }, localize('gettingStarted.skip', "Skip")), + // $('button.skip.button-link', { 'x-dispatch': 'skip' }, localize('gettingStarted.skip', "Skip")), $('p.showOnStartup', {}, showOnStartupCheckbox, $('label.caption', { for: 'showOnStartup' }, localize('welcomePage.showOnStartup', "Show Getting Started page on startup"))) @@ -384,6 +394,10 @@ export class GettingStartedPage extends EditorPane { const message = assertIsDefined(element.firstChild); const bar = assertIsDefined(element.querySelector('.progress-bar-inner')) as HTMLDivElement; + bar.setAttribute('aria-valuemin', '0'); + bar.setAttribute('aria-valuenow', '' + numDone); + bar.setAttribute('aria-valuemax', '' + numTotal); + bar.style.width = `${(numDone / numTotal) * 100}%`; if (numTotal === numDone) { @@ -429,6 +443,7 @@ export class GettingStartedPage extends EditorPane { 'x-dispatch': 'selectTask:' + task.id, 'data-task-id': task.id, 'aria-expanded': 'false', + 'role': 'listitem', }, $('.codicon' + (task.done ? '.complete.codicon-pass-filled' : '.codicon-circle-large-outline'), { 'data-done-task-id': task.id }), $('.task-description-container', {}, @@ -451,7 +466,7 @@ export class GettingStartedPage extends EditorPane { )) ))); - const detailContainer = $('.getting-started-detail-container'); + const detailContainer = $('.getting-started-detail-container', { 'role': 'list' }); if (this.detailsScrollbar) { this.detailsScrollbar.getDomNode().remove(); this.detailsScrollbar.dispose(); } this.detailsScrollbar = this._register(new DomScrollableElement(detailContainer, { className: 'full-height-scrollable' })); categoryElements.forEach(element => detailContainer.appendChild(element)); diff --git a/src/vs/workbench/electron-browser/desktop.main.ts b/src/vs/workbench/electron-browser/desktop.main.ts index a6b22ab4def..057696c9420 100644 --- a/src/vs/workbench/electron-browser/desktop.main.ts +++ b/src/vs/workbench/electron-browser/desktop.main.ts @@ -18,11 +18,12 @@ import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { INativeWorkbenchConfiguration, INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IWorkspaceInitializationPayload, reviveIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceInitializationPayload, reviveIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ILoggerService, ILogService } from 'vs/platform/log/common/log'; import { NativeStorageService } from 'vs/platform/storage/node/storageService'; +import { NativeStorageService2 } from 'vs/platform/storage/electron-sandbox/storageService2'; import { Schemas } from 'vs/base/common/network'; -import { GlobalStorageDatabaseChannelClient } from 'vs/platform/storage/node/storageIpc'; +import { StorageDatabaseChannelClient } from 'vs/platform/storage/common/storageIpc'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -83,7 +84,10 @@ class DesktopMain extends Disposable { private reviveUris() { // Workspace - this.configuration.workspace = reviveIdentifier(this.configuration.workspace); + const workspace = reviveIdentifier(this.configuration.workspace); + if (isWorkspaceIdentifier(workspace) || isSingleFolderWorkspaceIdentifier(workspace)) { + this.configuration.workspace = workspace; + } // Files const filesToWait = this.configuration.filesToWait; @@ -130,14 +134,14 @@ class DesktopMain extends Disposable { services.logService.trace('workbench configuration', JSON.stringify(this.configuration)); } - private registerListeners(workbench: Workbench, storageService: NativeStorageService): void { + private registerListeners(workbench: Workbench, storageService: NativeStorageService | NativeStorageService2): void { // Workbench Lifecycle this._register(workbench.onShutdown(() => this.dispose())); this._register(workbench.onWillShutdown(event => event.join(storageService.close(), 'join.closeStorage'))); } - private async initServices(): Promise<{ serviceCollection: ServiceCollection, logService: ILogService, storageService: NativeStorageService }> { + private async initServices(): Promise<{ serviceCollection: ServiceCollection, logService: ILogService, storageService: NativeStorageService | NativeStorageService2 }> { const serviceCollection = new ServiceCollection(); @@ -319,9 +323,14 @@ class DesktopMain extends Disposable { } } - private async createStorageService(payload: IWorkspaceInitializationPayload, logService: ILogService, mainProcessService: IMainProcessService): Promise { - const globalStorageDatabase = new GlobalStorageDatabaseChannelClient(mainProcessService.getChannel('storage')); - const storageService = new NativeStorageService(globalStorageDatabase, logService, this.environmentService); + private async createStorageService(payload: IWorkspaceInitializationPayload, logService: ILogService, mainProcessService: IMainProcessService): Promise { + let storageService: NativeStorageService | NativeStorageService2; + if (this.configuration.enableExperimentalMainProcessWorkspaceStorage) { + storageService = new NativeStorageService2(payload, mainProcessService, this.environmentService); + } else { + const storageDataBaseClient = new StorageDatabaseChannelClient(mainProcessService.getChannel('storage'), payload); + storageService = new NativeStorageService(storageDataBaseClient.globalStorage, logService, this.environmentService); + } try { await storageService.initialize(payload); diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index 299561eb5e1..1b1971fd14e 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -273,6 +273,12 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; 'scope': ConfigurationScope.APPLICATION, 'description': localize('window.clickThroughInactive', "If enabled, clicking on an inactive window will both activate the window and trigger the element under the mouse if it is clickable. If disabled, clicking anywhere on an inactive window will activate it only and a second click is required on the element."), 'included': isMacintosh + }, + 'window.enableExperimentalMainProcessWorkspaceStorage': { + 'type': 'boolean', + 'default': false, + 'scope': ConfigurationScope.APPLICATION, + 'description': localize('window.localize', "Enables workspace storage access from the main process. Requires a restart to take effect."), } } }); diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index d72468c610b..14d55ac5c31 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -9,35 +9,42 @@ import { Workbench } from 'vs/workbench/browser/workbench'; import { NativeWindow } from 'vs/workbench/electron-sandbox/window'; import { setZoomLevel, setZoomFactor, setFullscreen } from 'vs/base/browser/browser'; import { domContentLoaded } from 'vs/base/browser/dom'; +import { onUnexpectedError } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { INativeWorkbenchConfiguration, INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { reviveIdentifier } from 'vs/platform/workspaces/common/workspaces'; -import { ILogService } from 'vs/platform/log/common/log'; +import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceInitializationPayload, reviveIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { ILoggerService, ILogService } from 'vs/platform/log/common/log'; +import { NativeStorageService2 } from 'vs/platform/storage/electron-sandbox/storageService2'; import { Schemas } from 'vs/base/common/network'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { Disposable } from 'vs/base/common/lifecycle'; import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; +import { RemoteAuthorityResolverService } from 'vs/platform/remote/electron-sandbox/remoteAuthorityResolverService'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentServiceImpl'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { FileService } from 'vs/platform/files/common/fileService'; import { IFileService } from 'vs/platform/files/common/files'; import { RemoteFileSystemProvider } from 'vs/workbench/services/remote/common/remoteAgentFileSystemChannel'; import { ISignService } from 'vs/platform/sign/common/sign'; import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider'; +import { basename } from 'vs/base/common/path'; import { IProductService } from 'vs/platform/product/common/productService'; import product from 'vs/platform/product/common/product'; +import { NativeLogService } from 'vs/workbench/services/log/electron-sandbox/logService'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { NativeHostService } from 'vs/platform/native/electron-sandbox/nativeHostService'; -import { SimpleConfigurationService, simpleFileSystemProvider, SimpleLogService, SimpleSignService, SimpleStorageService, SimpleNativeWorkbenchEnvironmentService, SimpleWorkspaceService } from 'vs/workbench/electron-sandbox/sandbox.simpleservices'; -import { INativeWorkbenchConfiguration, INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; -import { RemoteAuthorityResolverService } from 'vs/platform/remote/electron-sandbox/remoteAuthorityResolverService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { UriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentityService'; +import { KeyboardLayoutService } from 'vs/workbench/services/keybinding/electron-sandbox/nativeKeyboardLayout'; +import { IKeyboardLayoutService } from 'vs/platform/keyboardLayout/common/keyboardLayout'; +import { LoggerService } from 'vs/workbench/services/log/electron-sandbox/loggerService'; import { ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { SimpleConfigurationService, simpleFileSystemProvider, SimpleSignService, SimpleNativeWorkbenchEnvironmentService, SimpleWorkspaceService } from 'vs/workbench/electron-sandbox/sandbox.simpleservices'; class DesktopMain extends Disposable { @@ -65,7 +72,10 @@ class DesktopMain extends Disposable { private reviveUris() { // Workspace - this.configuration.workspace = reviveIdentifier(this.configuration.workspace); + const workspace = reviveIdentifier(this.configuration.workspace); + if (isWorkspaceIdentifier(workspace) || isSingleFolderWorkspaceIdentifier(workspace)) { + this.configuration.workspace = workspace; + } // Files const filesToWait = this.configuration.filesToWait; @@ -107,14 +117,14 @@ class DesktopMain extends Disposable { services.logService.trace('workbench configuration', JSON.stringify(this.configuration)); } - private registerListeners(workbench: Workbench, storageService: SimpleStorageService): void { + private registerListeners(workbench: Workbench, storageService: NativeStorageService2): void { // Workbench Lifecycle this._register(workbench.onShutdown(() => this.dispose())); this._register(workbench.onWillShutdown(event => event.join(storageService.close(), 'join.closeStorage'))); } - private async initServices(): Promise<{ serviceCollection: ServiceCollection, logService: ILogService, storageService: SimpleStorageService }> { + private async initServices(): Promise<{ serviceCollection: ServiceCollection, logService: ILogService, storageService: NativeStorageService2 }> { const serviceCollection = new ServiceCollection(); @@ -142,8 +152,12 @@ class DesktopMain extends Disposable { // Product serviceCollection.set(IProductService, this.productService); + // Logger + const loggerService = new LoggerService(mainProcessService); + serviceCollection.set(ILoggerService, loggerService); + // Log - const logService = new SimpleLogService(); + const logService = this._register(new NativeLogService(`renderer${this.configuration.windowId}`, loggerService, mainProcessService, this.environmentService)); serviceCollection.set(ILogService, logService); // Remote @@ -209,6 +223,8 @@ class DesktopMain extends Disposable { fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider); } + const payload = this.resolveWorkspaceInitializationPayload(); + const services = await Promise.all([ this.createWorkspaceService().then(service => { @@ -221,11 +237,19 @@ class DesktopMain extends Disposable { return service; }), - this.createStorageService().then(service => { + this.createStorageService(payload, mainProcessService).then(service => { // Storage serviceCollection.set(IStorageService, service); + return service; + }), + + this.createKeyboardLayoutService(mainProcessService).then(service => { + + // KeyboardLayout + serviceCollection.set(IKeyboardLayoutService, service); + return service; }) ]); @@ -247,12 +271,56 @@ class DesktopMain extends Disposable { return { serviceCollection, logService, storageService: services[1] }; } + private resolveWorkspaceInitializationPayload(): IWorkspaceInitializationPayload { + let workspaceInitializationPayload: IWorkspaceInitializationPayload | undefined = this.configuration.workspace; + + // Fallback to empty workspace if we have no payload yet. + if (!workspaceInitializationPayload) { + let id: string; + if (this.configuration.backupPath) { + id = basename(this.configuration.backupPath); // we know the backupPath must be a unique path so we leverage its name as workspace ID + } else if (this.environmentService.isExtensionDevelopment) { + id = 'ext-dev'; // extension development window never stores backups and is a singleton + } else { + throw new Error('Unexpected window configuration without backupPath'); + } + + workspaceInitializationPayload = { id }; + } + + return workspaceInitializationPayload; + } + private async createWorkspaceService(): Promise { return new SimpleWorkspaceService(); } - private async createStorageService(): Promise { - return new SimpleStorageService(); + private async createStorageService(payload: IWorkspaceInitializationPayload, mainProcessService: IMainProcessService): Promise { + const storageService = new NativeStorageService2(payload, mainProcessService, this.environmentService); + + try { + await storageService.initialize(); + + return storageService; + } catch (error) { + onUnexpectedError(error); + + return storageService; + } + } + + private async createKeyboardLayoutService(mainProcessService: IMainProcessService): Promise { + const keyboardLayoutService = new KeyboardLayoutService(mainProcessService); + + try { + await keyboardLayoutService.initialize(); + + return keyboardLayoutService; + } catch (error) { + onUnexpectedError(error); + + return keyboardLayoutService; + } } } diff --git a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts index 4a2f05f35f9..7b7312d9575 100644 --- a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts +++ b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts @@ -6,7 +6,6 @@ /* eslint-disable code-no-standalone-editor */ /* eslint-disable code-import-patterns */ -import { ConsoleLogger, LogService } from 'vs/platform/log/common/log'; import { ISignService } from 'vs/platform/sign/common/sign'; import { URI } from 'vs/base/common/uri'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; @@ -15,7 +14,6 @@ import { IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnectio import { ITelemetryData, ITelemetryInfo, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtension } from 'vs/platform/extensions/common/extensions'; import { SimpleConfigurationService as BaseSimpleConfigurationService } from 'vs/editor/standalone/browser/simpleServices'; -import { InMemoryStorageService } from 'vs/platform/storage/common/storage'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup'; import { ITextSnapshot } from 'vs/editor/common/model'; @@ -177,13 +175,6 @@ export class SimpleWorkspaceService implements IWorkspaceContextService { //#endregion -//#region Configuration - -export class SimpleStorageService extends InMemoryStorageService { } - -//#endregion - - //#region Configuration export class SimpleConfigurationService extends BaseSimpleConfigurationService implements IWorkbenchConfigurationService { @@ -193,15 +184,7 @@ export class SimpleConfigurationService extends BaseSimpleConfigurationService i //#endregion -//#region Logger - -export class SimpleLogService extends LogService { - - constructor() { - super(new ConsoleLogger()); - } - -} +//#region Signing export class SimpleSignService implements ISignService { diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 5d6de318988..6c1efc704be 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -124,8 +124,7 @@ export interface IAuthenticationService { declaredProviders: AuthenticationProviderInformation[]; readonly onDidChangeDeclaredProviders: Event; - getSessions(id: string, scopes: string[], activateImmediate?: boolean): Promise>; - getAllSessions(providerId: string, activateImmediate?: boolean): Promise>; + getSessions(id: string, scopes?: string[], activateImmediate?: boolean): Promise>; getLabel(providerId: string): string; supportsMultipleAccounts(providerId: string): boolean; createSession(providerId: string, scopes: string[], activateImmediate?: boolean): Promise; @@ -694,16 +693,7 @@ export class AuthenticationService extends Disposable implements IAuthentication return Promise.race([didRegister, didTimeout]); } - async getAllSessions(id: string, activateImmediate: boolean = false): Promise> { - try { - const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, activateImmediate); - return await authProvider.getAllSessions(); - } catch (_) { - throw new Error(`No authentication provider '${id}' is currently registered.`); - } - } - - async getSessions(id: string, scopes: string[], activateImmediate: boolean = false): Promise> { + async getSessions(id: string, scopes?: string[], activateImmediate: boolean = false): Promise> { try { const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, activateImmediate); return await authProvider.getSessions(scopes); diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index 5577ffbb2b0..fa4552e36f9 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -17,7 +17,7 @@ import { Configuration } from 'vs/workbench/services/configuration/common/config import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, machineSettingsSchemaId, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions, allSettings, windowSettings, resourceSettings, applicationSettings, machineSettings, machineOverridableSettings, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { IWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IWorkspaceInitializationPayload, IEmptyWorkspaceInitializationPayload, useSlashForPath, getStoredWorkspaceFolder, isSingleFolderWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, toWorkspaceFolders } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IWorkspaceInitializationPayload, IEmptyWorkspaceIdentifier, useSlashForPath, getStoredWorkspaceFolder, isSingleFolderWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, toWorkspaceFolders } from 'vs/platform/workspaces/common/workspaces'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ConfigurationEditingService, EditableConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditingService'; import { WorkspaceConfiguration, FolderConfiguration, RemoteUserConfiguration, UserConfiguration } from 'vs/workbench/services/configuration/browser/configuration'; @@ -418,8 +418,8 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat return Promise.resolve(workspace); } - private createEmptyWorkspace(emptyWorkspacePayload: IEmptyWorkspaceInitializationPayload): Promise { - const workspace = new Workspace(emptyWorkspacePayload.id, [], null, uri => this.uriIdentityService.extUri.ignorePathCasing(uri)); + private createEmptyWorkspace(emptyWorkspaceIdentifier: IEmptyWorkspaceIdentifier): Promise { + const workspace = new Workspace(emptyWorkspaceIdentifier.id, [], null, uri => this.uriIdentityService.extUri.ignorePathCasing(uri)); workspace.initialized = true; return Promise.resolve(workspace); } diff --git a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts index 2d39c5bcd34..507dd8595b9 100644 --- a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts +++ b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts @@ -326,7 +326,8 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe default: try { - return this.resolveFromMap(match, variable, commandValueMapping, undefined); + const key = argument ? `${variable}:${argument}` : variable; + return this.resolveFromMap(match, key, commandValueMapping, undefined); } catch (error) { return match; } diff --git a/src/vs/workbench/services/gettingStarted/common/gettingStartedContent.ts b/src/vs/workbench/services/gettingStarted/common/gettingStartedContent.ts index f96301a0577..64e10c2eb92 100644 --- a/src/vs/workbench/services/gettingStarted/common/gettingStartedContent.ts +++ b/src/vs/workbench/services/gettingStarted/common/gettingStartedContent.ts @@ -40,6 +40,26 @@ type GettingStartedCategory = { type GettingStartedContent = GettingStartedCategory[]; export const content: GettingStartedContent = [ + // { + // id: 'topLevelCommandPalette', + // title: localize('gettingStarted.commandPalette.title', "Command Palette"), + // description: localize('gettingStarted.commandPalette.description', "The one keybinding to show you everything VS Code can do."), + // icon: Codicon.symbolColor, + // content: { + // type: 'command', + // command: 'workbench.action.showCommands', + // } + // }, + // { + // id: 'topLevelSeeExtensions', + // title: localize('gettingStarted.languageSupport.title', "Install Language Support"), + // description: localize('gettingStarted.languageSupport.description', "Want even more features? Install extensions to add support for languages like Python, C, or Java."), + // icon: Codicon.extensions, + // content: { + // type: 'command', + // command: 'workbench.extensions.action.showPopularExtensions', + // } + // }, { id: 'Codespaces', title: localize('gettingStarted.codespaces.title', "Primer on Codespaces"), diff --git a/src/vs/workbench/services/label/common/labelService.ts b/src/vs/workbench/services/label/common/labelService.ts index c9fc28409c0..550b36e9726 100644 --- a/src/vs/workbench/services/label/common/labelService.ts +++ b/src/vs/workbench/services/label/common/labelService.ts @@ -21,7 +21,6 @@ import { match } from 'vs/base/common/glob'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { hasDriveLetter } from 'vs/base/common/extpath'; const resourceLabelFormattersExtPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'resourceLabelFormatters', @@ -74,6 +73,10 @@ const resourceLabelFormattersExtPoint = ExtensionsRegistry.registerExtensionPoin const sepRegexp = /\//g; const labelMatchingRegexp = /\$\{(scheme|authority|path|(query)\.(.+?))\}/g; +function hasDriveLetterIgnorePlatform(path: string): boolean { + return !!(path && path[2] === ':'); +} + class ResourceLabelFormattersHandler implements IWorkbenchContribution { private formattersDisposables = new Map(); @@ -283,7 +286,7 @@ export class LabelService extends Disposable implements ILabelService { }); // convert \c:\something => C:\something - if (formatting.normalizeDriveLetter && hasDriveLetter(label.substr(1))) { + if (formatting.normalizeDriveLetter && hasDriveLetterIgnorePlatform(label)) { label = label.charAt(1).toUpperCase() + label.substr(2); } diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index e066dffffef..1ced7e337bf 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -17,6 +17,7 @@ import { IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnectio import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { isNumber, isObject, isString } from 'vs/base/common/types'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { hash } from 'vs/base/common/hash'; export const IRemoteExplorerService = createDecorator('remoteExplorerService'); export const REMOTE_EXPLORER_TYPE_KEY: string = 'remote.explorerType'; @@ -323,7 +324,8 @@ export class TunnelModel extends Disposable { private async getStorageKey(): Promise { const workspace = this.workspaceContextService.getWorkspace(); - return `${TUNNELS_TO_RESTORE}.${this.environmentService.remoteAuthority}.${workspace.id}`; + const workspaceHash = workspace.configuration ? hash(workspace.configuration.path) : (workspace.folders.length > 0 ? hash(workspace.folders[0].uri.path) : undefined); + return `${TUNNELS_TO_RESTORE}.${this.environmentService.remoteAuthority}.${workspaceHash}`; } private async getTunnelRestoreValue(): Promise { diff --git a/src/vs/workbench/services/search/electron-browser/searchService.ts b/src/vs/workbench/services/search/electron-browser/searchService.ts index 09160857d0f..9e1f8f3f913 100644 --- a/src/vs/workbench/services/search/electron-browser/searchService.ts +++ b/src/vs/workbench/services/search/electron-browser/searchService.ts @@ -61,10 +61,7 @@ export class DiskSearch implements ISearchResultProvider { serverName: 'Search', timeout, args: ['--type=searchService'], - // See https://github.com/microsoft/vscode/issues/27665 // Pass in fresh execArgv to the forked process such that it doesn't inherit them from `process.execArgv`. - // e.g. Launching the extension host process with `--inspect-brk=xxx` and then forking a process from the extension host - // results in the forked process inheriting `--inspect-brk=xxx`. freshExecArgv: true, env: { VSCODE_AMD_ENTRYPOINT: 'vs/workbench/services/search/node/searchApp', diff --git a/src/vs/workbench/services/timer/browser/timerService.ts b/src/vs/workbench/services/timer/browser/timerService.ts index f4bc25a6066..324de644b65 100644 --- a/src/vs/workbench/services/timer/browser/timerService.ts +++ b/src/vs/workbench/services/timer/browser/timerService.ts @@ -46,6 +46,7 @@ export interface IMemoryInfo { "timers.ellapsedExtensions" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "timers.ellapsedExtensionsReady" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "timers.ellapsedRequire" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "timers.ellapsedStorageInit" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "timers.ellapsedWorkspaceStorageInit" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "timers.ellapsedWorkspaceServiceInit" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "timers.ellapsedRequiredUserDataInit" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, @@ -215,6 +216,14 @@ export interface IStartupMetrics { */ readonly ellapsedWorkspaceStorageInit: number; + /** + * The time it took to init the storage database connection from the workbench. + * + * * Happens in the renderer-process + * * Measured with the `code/willInitStorage` and `code/didInitStorage` performance marks. + */ + readonly ellapsedStorageInit: number; + /** * The time it took to initialize the workspace and configuration service. * @@ -514,6 +523,7 @@ export abstract class AbstractTimerService implements ITimerService { ellapsedWindowLoadToRequire: this._marks.getDuration('code/willOpenNewWindow', 'code/willLoadWorkbenchMain'), ellapsedRequire: this._marks.getDuration('code/willLoadWorkbenchMain', 'code/didLoadWorkbenchMain'), ellapsedWaitForShellEnv: this._marks.getDuration('code/willWaitForShellEnv', 'code/didWaitForShellEnv'), + ellapsedStorageInit: this._marks.getDuration('code/willInitStorage', 'code/didInitStorage'), ellapsedWorkspaceStorageInit: this._marks.getDuration('code/willInitWorkspaceStorage', 'code/didInitWorkspaceStorage'), ellapsedWorkspaceServiceInit: this._marks.getDuration('code/willInitWorkspaceService', 'code/didInitWorkspaceService'), ellapsedRequiredUserDataInit: this._marks.getDuration('code/willInitRequiredUserData', 'code/didInitRequiredUserData'), diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 32228c4c3b0..81afe84f97c 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -202,7 +202,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat let accounts: Map = new Map(); let currentAccount: UserDataSyncAccount | null = null; - const sessions = await this.authenticationService.getAllSessions(authenticationProviderId) || []; + const sessions = await this.authenticationService.getSessions(authenticationProviderId) || []; for (const session of sessions) { const account: UserDataSyncAccount = new UserDataSyncAccount(authenticationProviderId, session); accounts.set(account.accountName, account); diff --git a/src/vs/workbench/services/workspaces/browser/workspacesService.ts b/src/vs/workbench/services/workspaces/browser/workspacesService.ts index 6d6b1d77002..28d39cec2f6 100644 --- a/src/vs/workbench/services/workspaces/browser/workspacesService.ts +++ b/src/vs/workbench/services/workspaces/browser/workspacesService.ts @@ -26,7 +26,7 @@ export class BrowserWorkspacesService extends Disposable implements IWorkspacesS declare readonly _serviceBrand: undefined; private readonly _onRecentlyOpenedChange = this._register(new Emitter()); - readonly onRecentlyOpenedChange = this._onRecentlyOpenedChange.event; + readonly onDidChangeRecentlyOpened = this._onRecentlyOpenedChange.event; constructor( @IStorageService private readonly storageService: IStorageService, diff --git a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts index 7619a7b337b..a85f77ba805 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts @@ -21,6 +21,7 @@ import { nullExtensionDescription } from 'vs/workbench/services/extensions/commo import { isEqual } from 'vs/base/common/resources'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import { generateUuid } from 'vs/base/common/uuid'; +import { Event } from 'vs/base/common/event'; suite('NotebookCell#Document', function () { @@ -33,9 +34,11 @@ suite('NotebookCell#Document', function () { const notebookUri = URI.parse('test:///notebook.file'); const disposables = new DisposableStore(); - setup(async function () { + teardown(function () { disposables.clear(); + }); + setup(async function () { rpcProtocol = new TestRPCProtocol(); rpcProtocol.set(MainContext.MainThreadCommands, new class extends mock() { $registerCommand() { } @@ -305,4 +308,49 @@ suite('NotebookCell#Document', function () { assert.strictEqual(notebook.notebookDocument.cells.length, 3); assert.strictEqual(second.index, 2); }); + + test('ERR MISSING extHostDocument for notebook cell: #116711', async function () { + + const p = Event.toPromise(extHostNotebooks.onDidChangeNotebookCells); + + // DON'T call this, make sure the cell-documents have not been created yet + // assert.strictEqual(notebook.notebookDocument.cells.length, 2); + + extHostNotebooks.$acceptModelChanged(notebook.uri, { + versionId: 100, + rawEvents: [{ + kind: NotebookCellsChangeType.ModelChange, + changes: [[0, 2, [{ + handle: 3, + uri: CellUri.generate(notebookUri, 3), + source: ['### Heading'], + eol: '\n', + language: 'markdown', + cellKind: CellKind.Markdown, + outputs: [], + }, { + handle: 4, + uri: CellUri.generate(notebookUri, 4), + source: ['console.log("aaa")', 'console.log("bbb")'], + eol: '\n', + language: 'javascript', + cellKind: CellKind.Code, + outputs: [], + }]]] + }] + }, false); + + assert.strictEqual(notebook.notebookDocument.cells.length, 2); + + const event = await p; + + assert.strictEqual(event.document === notebook.notebookDocument, true); + assert.strictEqual(event.changes.length, 1); + assert.strictEqual(event.changes[0].deletedCount, 2); + assert.strictEqual(event.changes[0].deletedItems[0].document.isClosed, true); + assert.strictEqual(event.changes[0].deletedItems[1].document.isClosed, true); + assert.strictEqual(event.changes[0].items.length, 2); + assert.strictEqual(event.changes[0].items[0].document.isClosed, false); + assert.strictEqual(event.changes[0].items[1].document.isClosed, false); + }); }); diff --git a/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts b/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts index 651add03080..22f2eeef016 100644 --- a/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts @@ -604,7 +604,7 @@ suite('ExtHostTreeView', function () { const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }, { enableProposedApi: true } as IExtensionDescription); return loadCompleteTree('treeDataProvider') .then(() => { - runWithEventMerging((resolve) => { + return runWithEventMerging((resolve) => { tree = { 'a': { 'aa': {}, @@ -633,9 +633,9 @@ suite('ExtHostTreeView', function () { .then(() => { assert.ok(revealTarget.calledOnce); assert.deepStrictEqual('treeDataProvider', revealTarget.args[0][0]); - assert.deepStrictEqual({ handle: '0/0:b/0:bc', label: { label: 'bc' }, collapsibleState: TreeItemCollapsibleState.None, parentHandle: '0/0:b' }, removeUnsetKeys(revealTarget.args[0][1])); - assert.deepStrictEqual([{ handle: '0/0:b', label: { label: 'b' }, collapsibleState: TreeItemCollapsibleState.Collapsed }], (>revealTarget.args[0][2]).map(arg => removeUnsetKeys(arg))); - assert.deepStrictEqual({ select: true, focus: false, expand: false }, revealTarget.args[0][3]); + assert.deepStrictEqual({ handle: '0/0:b/0:bc', label: { label: 'bc' }, collapsibleState: TreeItemCollapsibleState.None, parentHandle: '0/0:b' }, removeUnsetKeys(revealTarget.args[0][1].item)); + assert.deepStrictEqual([{ handle: '0/0:b', label: { label: 'b' }, collapsibleState: TreeItemCollapsibleState.Collapsed }], (>revealTarget.args[0][1].parentChain).map(arg => removeUnsetKeys(arg))); + assert.deepStrictEqual({ select: true, focus: false, expand: false }, revealTarget.args[0][2]); }); }); }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index e2d267e6704..aa394668a9b 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1413,7 +1413,7 @@ export class TestTextFileEditorModelManager extends TextFileEditorModelManager { export class TestWorkspacesService implements IWorkspacesService { _serviceBrand: undefined; - onRecentlyOpenedChange = Event.None; + onDidChangeRecentlyOpened = Event.None; async createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise { throw new Error('Method not implemented.'); } async deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise { } diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index f2c26043db4..6dbb50fa818 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -55,6 +55,7 @@ export const TestWorkbenchConfiguration: INativeWorkbenchConfiguration = { perfMarks: [], colorScheme: { dark: true, highContrast: false }, os: { release: release() }, + enableExperimentalMainProcessWorkspaceStorage: false, ...parseArgs(process.argv, OPTIONS) }; diff --git a/test/smoke/src/areas/notebook/notebook.test.ts b/test/smoke/src/areas/notebook/notebook.test.ts index 27f8269954b..1d70ee93a89 100644 --- a/test/smoke/src/areas/notebook/notebook.test.ts +++ b/test/smoke/src/areas/notebook/notebook.test.ts @@ -34,7 +34,7 @@ export function setup() { await app.workbench.notebook.stopEditingCell(); }); - it.skip('inserts/edits markdown cell', async function () { + it('inserts/edits markdown cell', async function () { const app = this.app as Application; await app.workbench.notebook.openNotebook(); await app.workbench.notebook.focusNextCell(); diff --git a/test/ui/tree/public/compressed.json b/test/ui/tree/public/compressed.json index c0b5d4d7161..939a0a29145 100644 --- a/test/ui/tree/public/compressed.json +++ b/test/ui/tree/public/compressed.json @@ -148,7 +148,7 @@ "children": [ { "element": { - "name": "master" + "name": "main" }, "incompressible": true } @@ -228,7 +228,7 @@ "children": [ { "element": { - "name": "master" + "name": "main" }, "incompressible": true } @@ -15617,4 +15617,4 @@ } ] } -] \ No newline at end of file +]